mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2026-05-03 07:50:46 +00:00
Chaos spam data (#863)
* create base component * add as alpha * Add hotkey action * updating config stuff * Adds writing csv * ui responsive * Style updates * smor style updates * fix issue with recreating file * compute diff * general cleanup * rebuild into secondary widget * Remove builtin error * add a unit test * fix spelling? * add custom file path * Add folder option
This commit is contained in:
+19
-2
@@ -9,6 +9,8 @@ export interface HostConfig {
|
||||
windowTitle: string;
|
||||
language: string;
|
||||
readClientLog: boolean;
|
||||
libraryAlpha: boolean;
|
||||
libraryOutputPath: string | null;
|
||||
}
|
||||
|
||||
export interface ShortcutAction {
|
||||
@@ -88,11 +90,12 @@ export type IpcEvent =
|
||||
| IpcItemText
|
||||
| IpcOcrText
|
||||
| IpcConfigChanged
|
||||
| IpcUserAction;
|
||||
| IpcUserAction
|
||||
| IpcWriteCsv;
|
||||
|
||||
export type IpcEventPayload<
|
||||
Name extends IpcEvent["name"],
|
||||
T extends IpcEvent = IpcEvent
|
||||
T extends IpcEvent = IpcEvent,
|
||||
> = T extends { name: Name; payload: infer P } ? P : never;
|
||||
|
||||
type IpcOverlayAttached = Event<"MAIN->OVERLAY::overlay-attached">;
|
||||
@@ -209,6 +212,20 @@ type IpcUserAction = Event<
|
||||
}
|
||||
>;
|
||||
|
||||
type IpcWriteCsv = Event<
|
||||
"CLIENT->MAIN::write-data",
|
||||
| {
|
||||
action: "log-item";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
action: "session";
|
||||
start: boolean;
|
||||
name?: string;
|
||||
header?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
interface Event<TName extends string, TPayload = undefined> {
|
||||
name: TName;
|
||||
payload: TPayload;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import { ServerEvents } from "../server";
|
||||
import { promises as fs, existsSync } from "fs";
|
||||
import { Logger } from "../RemoteLogger";
|
||||
|
||||
export class FileWriter {
|
||||
private defaultUploadsPath = path.join(
|
||||
app.getPath("userData"),
|
||||
"apt-data",
|
||||
"csv-data",
|
||||
);
|
||||
|
||||
private uploadsPath = this.defaultUploadsPath;
|
||||
|
||||
private _state: {
|
||||
file: fs.FileHandle;
|
||||
} | null = null;
|
||||
|
||||
private _enabled = false;
|
||||
|
||||
constructor(
|
||||
private server: ServerEvents,
|
||||
private logger: Logger,
|
||||
) {
|
||||
this.server.onEventAnyClient("CLIENT->MAIN::write-data", async (e) => {
|
||||
if (!this._enabled) return;
|
||||
if (e.action !== "log-item" && e.action !== "session") return;
|
||||
if (e.action === "log-item") {
|
||||
this.writeLine(e.text);
|
||||
return;
|
||||
}
|
||||
// e.action === "session"
|
||||
if (e.start) {
|
||||
if (!e.name || !e.header) {
|
||||
this.logger.write("error [FileWriter] Invalid session start event.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.writeSessionStart(e.name, e.header);
|
||||
} else {
|
||||
this.writeSessionEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async restart(enabled: boolean, outputPath: string | null) {
|
||||
this._enabled = enabled;
|
||||
this.uploadsPath = outputPath ?? this.defaultUploadsPath;
|
||||
if (!this.uploadsPath.length) {
|
||||
this.uploadsPath = this.defaultUploadsPath;
|
||||
}
|
||||
}
|
||||
|
||||
private async writeSessionStart(name: string, header: string) {
|
||||
try {
|
||||
if (!existsSync(this.uploadsPath)) {
|
||||
await fs.mkdir(this.uploadsPath, { recursive: true });
|
||||
}
|
||||
const filePath = path.join(this.uploadsPath, name + ".csv");
|
||||
if (!existsSync(filePath)) {
|
||||
const file = await fs.open(filePath, "w");
|
||||
this._state = { file };
|
||||
|
||||
this.writeLine(header);
|
||||
} else {
|
||||
const file = await fs.open(filePath, "a");
|
||||
this._state = { file };
|
||||
}
|
||||
} catch {
|
||||
this.logger.write("error [FileWriter] Failed to create session file.");
|
||||
}
|
||||
}
|
||||
|
||||
private writeSessionEnd() {
|
||||
if (!this._state) return;
|
||||
|
||||
this._state.file.close();
|
||||
this._state = null;
|
||||
}
|
||||
|
||||
private writeLine(line: string) {
|
||||
if (!this._state) return;
|
||||
|
||||
this._state.file.write(line + "\n");
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { OverlayVisibility } from "./windowing/OverlayVisibility";
|
||||
import { GameLogWatcher } from "./host-files/GameLogWatcher";
|
||||
import { HttpProxy } from "./proxy";
|
||||
import { installExtension, VUEJS_DEVTOOLS } from "electron-devtools-installer";
|
||||
import { FileWriter } from "./host-files/FileWriter";
|
||||
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
@@ -70,6 +71,7 @@ let tray: AppTray;
|
||||
const appUpdater = new AppUpdater(eventPipe);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _httpProxy = new HttpProxy(server, logger);
|
||||
const fileWriter = new FileWriter(eventPipe, logger);
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
try {
|
||||
@@ -114,6 +116,7 @@ let tray: AppTray;
|
||||
gameConfig.readConfig(cfg.gameConfig ?? "");
|
||||
appUpdater.checkAtStartup();
|
||||
tray.overlayKey = cfg.overlayKey;
|
||||
fileWriter.restart(cfg.libraryAlpha, cfg.libraryOutputPath);
|
||||
},
|
||||
);
|
||||
uIOhook.start();
|
||||
|
||||
@@ -334,8 +334,6 @@
|
||||
"show_overlay_ready": "Benachrichtigung anzeigen, wenn Overlay ein PoE-Fenster erkennt",
|
||||
"debug_hotkeys": "Alle Tastenanschläge aufzeichnen",
|
||||
"preferred_trade_site": "Bevorzugte Handelsseite",
|
||||
"enable_alphas": "Alphas aktivieren",
|
||||
"alphas_warning": "Alphas sind extrem experimentell und werden wahrscheinlich nicht wie erwartet funktionieren. Bitte melden Sie alle auftretenden Probleme.",
|
||||
"help": "Hilfe",
|
||||
"show_tips_on_startup": "Tipps bei Startbenachrichtigung anzeigen",
|
||||
"show_tips_frequency": "Wie oft sollen Tipps beim Preis-Check angezeigt werden?",
|
||||
|
||||
@@ -373,8 +373,8 @@
|
||||
"show_overlay_ready": "Show a notification when the Overlay detects a PoE window",
|
||||
"debug_hotkeys": "Record all key presses",
|
||||
"preferred_trade_site": "Preferred trade site",
|
||||
"enable_alphas": "Enable alphas",
|
||||
"alphas_warning": "Alphas are extremely experimental and will probably not work as expected. Please report any issues you encounter.",
|
||||
"enable_alphas": "Enable experimental features",
|
||||
"alpha_library": "Item Data Collection",
|
||||
"help": "Help",
|
||||
"show_tips_on_startup": "Show tips on startup notification",
|
||||
"show_tips_frequency": "How often to show tips when price checking",
|
||||
@@ -478,5 +478,13 @@
|
||||
"exp_tracking_help": "XP tracking widget shows your current level, current experience multiplier, and how close you are to being under-leveled. XP penalty is applied immediately if you are over-leveled, and when you are under-leveled by \u230A level/16 + 3 \u230B. The 'Over' is how many levels the your current level is over the bottom of the 'safe zone', generally 0 or 1 means you are killing the highest level monsters you can without incurring the under-leveled penalty",
|
||||
"exp_inspire": "Based on ",
|
||||
"exp_astrict": "*Experience gain percentages, when over level 95, or monster level over 70, or potentially when not at 100% exp gain, may not be fully tested and accurate."
|
||||
},
|
||||
"library": {
|
||||
"name": "Library",
|
||||
"log_item_key": "Log item to csv",
|
||||
"output_file": "Output csv path",
|
||||
"output_folder": "Output Folder",
|
||||
"output_file_help": "Some info text",
|
||||
"record_count": "Session Rolls:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,9 +332,7 @@
|
||||
"restore_clipboard": "Restaurar portapapeles",
|
||||
"show_overlay_ready": "Mostrar una notificación cuando la Superposición detecte una ventana de PoE",
|
||||
"debug_hotkeys": "Registrar todas las teclas presionadas",
|
||||
"preferred_trade_site": "Sitio de comercio preferido",
|
||||
"enable_alphas": "Habilitar alfas",
|
||||
"alphas_warning": "Las alfas son extremadamente experimentales y probablemente no funcionarán como se espera. Por favor, reporta cualquier problema que encuentres."
|
||||
"preferred_trade_site": "Sitio de comercio preferido"
|
||||
},
|
||||
"price_check": {
|
||||
"name": "Comprobación de precios",
|
||||
|
||||
@@ -354,8 +354,6 @@
|
||||
"show_overlay_ready": "Mostrar uma notificação quando a Sobreposição detectar uma janela do PoE",
|
||||
"debug_hotkeys": "Registrar todas as teclas pressionadas",
|
||||
"preferred_trade_site": "Site de comércio preferido",
|
||||
"enable_alphas": "Habilitar versões alfas",
|
||||
"alphas_warning": "As versões alfas são extremamente experimentais e provavelmente não funcionarão como esperado. Por favor, reporte qualquer problema que encontrar.",
|
||||
"help": "Ajuda",
|
||||
"show_tips_on_startup": "Mostrar dicas na notificação de inicialização",
|
||||
"show_tips_frequency": "Frequência das dicas ao checar preço",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ParsedModifier } from "@/parser/advanced-mod-desc";
|
||||
import { ModifierType } from "@/parser/modifiers";
|
||||
import { __testExports, ColumnOpts } from "@/web/library/widget";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("modFilter", () => {
|
||||
it.each([
|
||||
[{ explicit: true, implicit: false, enchant: false, augment: false }, [0]],
|
||||
[
|
||||
{ explicit: true, implicit: false, enchant: true, augment: false },
|
||||
[0, 2],
|
||||
],
|
||||
[
|
||||
{ explicit: true, implicit: false, enchant: false, augment: true },
|
||||
[0, 3],
|
||||
],
|
||||
[
|
||||
{ explicit: true, implicit: false, enchant: true, augment: true },
|
||||
[0, 2, 3],
|
||||
],
|
||||
[
|
||||
{ explicit: true, implicit: true, enchant: false, augment: false },
|
||||
[0, 1],
|
||||
],
|
||||
])(
|
||||
"Returns correct mods",
|
||||
(filter: Record<string, boolean>, expected: number[]) => {
|
||||
const mods = [
|
||||
{ info: { generation: "suffix" }, stats: [] },
|
||||
{ info: { type: ModifierType.Implicit }, stats: [] },
|
||||
{ info: { type: ModifierType.Enchant }, stats: [] },
|
||||
{ info: { type: ModifierType.Augment }, stats: [] },
|
||||
];
|
||||
const set = new Set(expected);
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
expect(
|
||||
__testExports.modFilter(
|
||||
mods[i] as unknown as ParsedModifier,
|
||||
filter as unknown as ColumnOpts["keep"],
|
||||
),
|
||||
).toBe(set.has(i));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -148,6 +148,23 @@ export function translateStatWithRoll(
|
||||
};
|
||||
}
|
||||
|
||||
export function modsEqual(a: ParsedModifier, b: ParsedModifier) {
|
||||
return (
|
||||
a.stats.length === b.stats.length &&
|
||||
a.info.name === b.info.name &&
|
||||
a.info.tier === b.info.tier &&
|
||||
a.info.generation === b.info.generation &&
|
||||
a.info.type === b.info.type &&
|
||||
a.stats.every(
|
||||
(statA, i) =>
|
||||
statA.stat.ref === b.stats[i].stat.ref &&
|
||||
statA.roll?.min === b.stats[i].roll?.min &&
|
||||
statA.roll?.max === b.stats[i].roll?.max &&
|
||||
statA.roll?.value === b.stats[i].roll?.value,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export enum ModifierType {
|
||||
Pseudo = "pseudo",
|
||||
Explicit = "explicit",
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { StashSearchWidget } from "./stash-search/widget";
|
||||
import type { ItemCheckWidget } from "./item-check/widget";
|
||||
import type { ItemSearchWidget } from "./item-search/widget";
|
||||
import { registry as widgetRegistry } from "./overlay/widget-registry.js";
|
||||
import { LibraryWidget } from "./library/widget";
|
||||
|
||||
const _config = shallowRef<Config | null>(null);
|
||||
let _lastSavedConfig: Config | null = null;
|
||||
@@ -147,7 +148,7 @@ export interface Config {
|
||||
showAttachNotification: boolean;
|
||||
overlayAlwaysClose: boolean;
|
||||
enableAlphas: boolean;
|
||||
alphas: [];
|
||||
alphas: Array<"library">;
|
||||
tipsFrequency: TipsFrequency;
|
||||
readClientLog: boolean; // default to false, opt-in only
|
||||
}
|
||||
@@ -620,6 +621,21 @@ function upgradeConfig(_config: Config): Config {
|
||||
|
||||
config.configVersion = 29;
|
||||
}
|
||||
|
||||
if (config.configVersion < 30) {
|
||||
// NOTE: v0.13.11 || poe0.4.0d
|
||||
const itemSearchId: number = config.widgets.find(
|
||||
(w) => w.wmType === "item-search",
|
||||
)!.wmId;
|
||||
// splicing to insert after the item-search widget, for positioning on the main overlay
|
||||
config.widgets.splice(itemSearchId, 0, {
|
||||
...defaultConfig().widgets.find((w) => w.wmType === "library")!,
|
||||
wmId: Math.max(0, ...config.widgets.map((_) => _.wmId)) + 1,
|
||||
});
|
||||
|
||||
config.configVersion = 30;
|
||||
}
|
||||
|
||||
return config as unknown as Config;
|
||||
}
|
||||
|
||||
@@ -682,6 +698,15 @@ function getConfigForHost(): HostConfig {
|
||||
action: { type: "copy-item", target: "item-check", focusOverlay: true },
|
||||
});
|
||||
}
|
||||
const library = AppConfig("library") as LibraryWidget;
|
||||
if (library.logItemKey) {
|
||||
actions.push({
|
||||
shortcut: library.logItemKey,
|
||||
keepModKeys: true,
|
||||
action: { type: "copy-item", target: "log-item" },
|
||||
});
|
||||
}
|
||||
|
||||
const delveGrid = AppConfig("delve-grid") as widget.DelveGridWidget;
|
||||
if (delveGrid.toggleKey) {
|
||||
actions.push({
|
||||
@@ -760,5 +785,7 @@ function getConfigForHost(): HostConfig {
|
||||
windowTitle: config.windowTitle,
|
||||
language: config.language,
|
||||
readClientLog: config.readClientLog,
|
||||
libraryAlpha: config.enableAlphas && config.alphas.includes("library"),
|
||||
libraryOutputPath: library.libraryOutputPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-y-1 overflow-y-auto min-h-0">
|
||||
<div :class="[$style.dataField, $style.dataFieldRow]">
|
||||
<div>{{ t(":record_count") }}</div>
|
||||
<div :class="$style.numericField">
|
||||
{{ rollCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="modsDiff.length"
|
||||
:class="[$style.dataField, $style.dataFieldColumn]"
|
||||
>
|
||||
<div v-for="mod in modsDiff">{{ mod }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="errMessage && errMessage !== ''"
|
||||
class="flex-row bg-orange-700"
|
||||
:class="[$style.dataField, $style.dataFieldColumn]"
|
||||
>
|
||||
<div>{{ errMessage }}</div>
|
||||
<button
|
||||
@click="errMessage = null"
|
||||
class="w-6 h-6 min-w-6 min-h-6 max-w-6 max-h-6"
|
||||
:class="$style.button"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { useI18nNs } from "@/web/i18n";
|
||||
import { ParsedModifier } from "@/parser/advanced-mod-desc";
|
||||
import { parseClipboard, ParsedItem } from "@/parser";
|
||||
import { buildCsvString, ColumnOpts, diffItem } from "./widget";
|
||||
import { ok } from "neverthrow";
|
||||
import { Host, MainProcess } from "@/web/background/IPC";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SingleItemSession",
|
||||
props: {
|
||||
libEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
sessionType: {
|
||||
type: String as PropType<"chaos">,
|
||||
required: true,
|
||||
},
|
||||
currentOpts: {
|
||||
type: Object as PropType<ColumnOpts>,
|
||||
required: true,
|
||||
},
|
||||
inSession: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const item = ref<ParsedItem | null>(null);
|
||||
const itemModsDiff = ref<{
|
||||
added: ParsedModifier[];
|
||||
removed: ParsedModifier[];
|
||||
}>({ added: [], removed: [] });
|
||||
const rollCount = shallowRef<number>(0);
|
||||
const errMessage = ref<string | null>(null);
|
||||
|
||||
watch(
|
||||
() => props.inSession,
|
||||
(curr) => {
|
||||
if (!curr) {
|
||||
item.value = null;
|
||||
rollCount.value = 0;
|
||||
errMessage.value = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
item,
|
||||
(curr, prev) => {
|
||||
if (!curr) return;
|
||||
|
||||
itemModsDiff.value = diffItem(curr, prev);
|
||||
|
||||
buildCsvString(
|
||||
curr,
|
||||
props.sessionType,
|
||||
itemModsDiff.value.added,
|
||||
itemModsDiff.value.removed,
|
||||
{
|
||||
columnOpts: props.currentOpts,
|
||||
},
|
||||
)
|
||||
.andThen((text) => {
|
||||
Host.sendEvent({
|
||||
name: "CLIENT->MAIN::write-data",
|
||||
payload: {
|
||||
action: "log-item",
|
||||
text,
|
||||
},
|
||||
});
|
||||
rollCount.value++;
|
||||
return ok(null);
|
||||
})
|
||||
.mapErr((err) => {
|
||||
errMessage.value = err;
|
||||
console.warn(err);
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
MainProcess.onEvent("MAIN->CLIENT::item-text", (e) => {
|
||||
if (e.target !== "log-item") return;
|
||||
if (!props.libEnabled) return;
|
||||
if (!props.inSession) return;
|
||||
|
||||
performance.mark("log-item-event");
|
||||
const res = parseClipboard(e.clipboard);
|
||||
if (res.isErr()) {
|
||||
errMessage.value = res.error;
|
||||
item.value = null;
|
||||
return;
|
||||
}
|
||||
item.value = res.value;
|
||||
performance.mark("log-item-parsed");
|
||||
});
|
||||
|
||||
const { t } = useI18nNs("library");
|
||||
return {
|
||||
t,
|
||||
item,
|
||||
rollCount,
|
||||
modsDiff: computed(() => {
|
||||
if (itemModsDiff.value.added.length === 0) return ["None added"];
|
||||
return itemModsDiff.value.added.map((mod) => mod.info.name);
|
||||
}),
|
||||
errMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" module>
|
||||
.button {
|
||||
@apply bg-gray-800;
|
||||
@apply rounded;
|
||||
line-height: 1;
|
||||
@apply w-8 h-8;
|
||||
@apply max-w-8 max-h-8;
|
||||
@apply min-w-8 min-h-8;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.dataField {
|
||||
flex-shrink: 0;
|
||||
@apply rounded;
|
||||
@apply max-w-sm;
|
||||
@apply p-2 leading-4;
|
||||
@apply text-gray-100 bg-gray-800;
|
||||
@apply content-center items-center justify-between;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dataFieldRow {
|
||||
@apply flex flex-row;
|
||||
}
|
||||
.dataFieldColumn {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.numericField {
|
||||
@apply text-center p-1;
|
||||
@apply bg-transparent text-gray-300;
|
||||
@apply rounded border-2 border-gray-900;
|
||||
@apply min-w-8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<Widget
|
||||
:config="config"
|
||||
:removable="false"
|
||||
:inline-edit="false"
|
||||
move-handles="top-bottom"
|
||||
>
|
||||
<div
|
||||
class="widget-default-style p-1 flex flex-col overflow-y-auto min-h-0 min-w-48"
|
||||
>
|
||||
<div class="text-gray-100 p-1 flex items-center justify-between gap-4">
|
||||
<div
|
||||
v-if="inSession"
|
||||
class="text-gray-100 m-1 leading-4 w-full text-center p-1"
|
||||
>
|
||||
{{ sessionName }}
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
class="leading-4 rounded text-gray-100 p-1 bg-gray-700 w-full mb-1"
|
||||
v-model="sessionName"
|
||||
/>
|
||||
<button v-if="!inSession" @click="startSession" :class="$style.button">
|
||||
<i class="fas fa-play"></i>
|
||||
</button>
|
||||
<button v-else @click="endSession" :class="$style.button">
|
||||
<i class="fas fa-stop"></i>
|
||||
</button>
|
||||
</div>
|
||||
<single-item-session
|
||||
:session-type="sessionType"
|
||||
:current-opts="currentOpts"
|
||||
:in-session="inSession"
|
||||
:lib-enabled="libEnabled"
|
||||
/>
|
||||
<div
|
||||
v-if="errMessage && errMessage !== ''"
|
||||
class="flex-row bg-orange-700 mt-1"
|
||||
:class="[$style.dataField, $style.dataFieldColumn]"
|
||||
>
|
||||
<div>{{ errMessage }}</div>
|
||||
<button
|
||||
@click="errMessage = null"
|
||||
class="w-6 h-6 min-w-6 min-h-6 max-w-6 max-h-6"
|
||||
:class="$style.button"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Widget>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType, shallowRef, watch } from "vue";
|
||||
|
||||
import Widget from "@/web/overlay/Widget.vue";
|
||||
import { WidgetSpec } from "@/web/overlay/interfaces";
|
||||
import { ColumnOpts, getHeader, LibraryWidget } from "./widget";
|
||||
import { Host } from "@/web/background/IPC";
|
||||
import { AppConfig } from "@/web/Config";
|
||||
import { useI18nNs } from "@/web/i18n";
|
||||
import SingleItemSession from "./SingleItemSession.vue";
|
||||
|
||||
function startSessionHost(name: string, header: string) {
|
||||
Host.sendEvent({
|
||||
name: "CLIENT->MAIN::write-data",
|
||||
payload: {
|
||||
action: "session",
|
||||
start: true,
|
||||
name,
|
||||
header,
|
||||
},
|
||||
});
|
||||
}
|
||||
function endSessionHost() {
|
||||
Host.sendEvent({
|
||||
name: "CLIENT->MAIN::write-data",
|
||||
payload: {
|
||||
action: "session",
|
||||
start: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
widget: {
|
||||
type: "library",
|
||||
instances: "single",
|
||||
initInstance: (): LibraryWidget => {
|
||||
return {
|
||||
wmId: 0,
|
||||
wmType: "library",
|
||||
wmTitle: "{icon=fa-book}",
|
||||
wmWants: "hide",
|
||||
wmZorder: null,
|
||||
wmFlags: ["invisible-on-blur", "menu::skip"],
|
||||
anchor: {
|
||||
pos: "tl",
|
||||
x: 20,
|
||||
y: 20,
|
||||
},
|
||||
logItemKey: null,
|
||||
libraryOutputPath: null,
|
||||
profiles: {
|
||||
chaos: {
|
||||
refName: true,
|
||||
itemLevel: true,
|
||||
rarity: true,
|
||||
sockets: true,
|
||||
mods: true,
|
||||
addedMods: true,
|
||||
removedMods: true,
|
||||
keep: {
|
||||
explicit: true,
|
||||
implicit: false,
|
||||
enchant: false,
|
||||
augment: false,
|
||||
},
|
||||
modOpts: {
|
||||
name: true,
|
||||
tier: true,
|
||||
roll: true,
|
||||
ref: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies WidgetSpec,
|
||||
components: { Widget, SingleItemSession },
|
||||
props: {
|
||||
config: {
|
||||
type: Object as PropType<LibraryWidget>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const libEnabled = computed(
|
||||
() => AppConfig().enableAlphas && AppConfig().alphas.includes("library"),
|
||||
);
|
||||
|
||||
const inSession = shallowRef<boolean>(false);
|
||||
|
||||
const sessionName = shallowRef<string>("mySession");
|
||||
const sessionType = shallowRef<"chaos">("chaos");
|
||||
const currentOpts = shallowRef<ColumnOpts>(props.config.profiles.chaos);
|
||||
const errMessage = shallowRef<string | null>();
|
||||
|
||||
watch(libEnabled, (curr) => {
|
||||
if (!curr) {
|
||||
endSessionHost();
|
||||
inSession.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// update selected options when user changes settings for profiles or switches profiles
|
||||
watch(
|
||||
props.config.profiles,
|
||||
() => {
|
||||
currentOpts.value = props.config.profiles[sessionType.value];
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
watch(sessionType, () => {
|
||||
currentOpts.value = props.config.profiles[sessionType.value];
|
||||
});
|
||||
|
||||
function startSession() {
|
||||
const header = getHeader(sessionType.value);
|
||||
|
||||
if (header.isErr()) {
|
||||
errMessage.value = header.error;
|
||||
return;
|
||||
}
|
||||
startSessionHost(sessionName.value, header.value);
|
||||
inSession.value = true;
|
||||
props.config.wmFlags = props.config.wmFlags.filter(
|
||||
(f) => f !== "invisible-on-blur",
|
||||
);
|
||||
}
|
||||
function endSession() {
|
||||
endSessionHost();
|
||||
inSession.value = false;
|
||||
if (!props.config.wmFlags.includes("invisible-on-blur")) {
|
||||
props.config.wmFlags.push("invisible-on-blur");
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18nNs("library");
|
||||
|
||||
return {
|
||||
t,
|
||||
startSession,
|
||||
endSession,
|
||||
libEnabled,
|
||||
inSession,
|
||||
sessionName,
|
||||
sessionType,
|
||||
currentOpts,
|
||||
errMessage,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
endSessionHost();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="postcss" module>
|
||||
.button {
|
||||
@apply bg-gray-800;
|
||||
@apply rounded;
|
||||
line-height: 1;
|
||||
@apply w-8 h-8;
|
||||
@apply max-w-8 max-h-8;
|
||||
@apply min-w-8 min-h-8;
|
||||
|
||||
&:hover {
|
||||
@apply bg-gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.dataField {
|
||||
flex-shrink: 0;
|
||||
@apply rounded;
|
||||
@apply max-w-sm;
|
||||
@apply p-2 leading-4;
|
||||
@apply text-gray-100 bg-gray-800;
|
||||
@apply content-center items-center justify-between;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dataFieldRow {
|
||||
@apply flex flex-row;
|
||||
}
|
||||
.dataFieldColumn {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.numericField {
|
||||
@apply text-center p-1;
|
||||
@apply bg-transparent text-gray-300;
|
||||
@apply rounded border-2 border-gray-900;
|
||||
@apply min-w-8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-2 max-w-md">
|
||||
<div class="flex mb-4">
|
||||
<label class="flex-1">{{ t(":log_item_key") }}</label>
|
||||
<hotkey-input v-model="logItemKey" class="w-48" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex-1 mb-1">{{ t(":output_folder") }}</div>
|
||||
<input
|
||||
v-model.trim="outputFolder"
|
||||
class="rounded bg-gray-900 px-1 block w-full font-sans"
|
||||
placeholder="...?/AppData/Roaming/exiled-exchange-2/apt-data/csv-data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "vue";
|
||||
import { useI18nNs } from "@/web/i18n";
|
||||
import { configModelValue, configProp, findWidget } from "../settings/utils.js";
|
||||
import { LibraryWidget } from "./widget.js";
|
||||
import HotkeyInput from "../settings/HotkeyInput.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "library.name",
|
||||
components: { HotkeyInput },
|
||||
props: configProp(),
|
||||
setup(props) {
|
||||
const configLibraryWidget = computed(
|
||||
() => findWidget<LibraryWidget>("library", props.config)!,
|
||||
);
|
||||
|
||||
const { t } = useI18nNs("library");
|
||||
|
||||
return {
|
||||
t,
|
||||
logItemKey: configModelValue(
|
||||
() => configLibraryWidget.value,
|
||||
"logItemKey",
|
||||
),
|
||||
outputFolder: configModelValue(
|
||||
() => configLibraryWidget.value,
|
||||
"libraryOutputPath",
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,199 @@
|
||||
import { err, ok, Result } from "neverthrow";
|
||||
import { Anchor, Widget } from "@/web/overlay/widgets";
|
||||
import { ParsedItem } from "@/parser";
|
||||
import { ModifierType, modsEqual } from "@/parser/modifiers";
|
||||
import { ParsedModifier } from "@/parser/advanced-mod-desc";
|
||||
|
||||
export interface LibraryWidget extends Widget {
|
||||
anchor: Anchor;
|
||||
logItemKey: string | null;
|
||||
libraryOutputPath: string | null;
|
||||
profiles: Record<string, ColumnOpts>;
|
||||
}
|
||||
|
||||
export interface ShortMod {
|
||||
name?: string;
|
||||
tier?: number;
|
||||
roll: Array<number | -999>;
|
||||
ref: string[];
|
||||
type: string;
|
||||
}
|
||||
export interface ColumnOpts {
|
||||
refName: true;
|
||||
itemLevel: true;
|
||||
rarity: true;
|
||||
sockets: true;
|
||||
mods: true;
|
||||
addedMods: boolean;
|
||||
removedMods: boolean;
|
||||
keep: {
|
||||
explicit: boolean;
|
||||
implicit: boolean;
|
||||
enchant: boolean;
|
||||
augment: boolean;
|
||||
};
|
||||
modOpts: {
|
||||
name: true;
|
||||
tier: boolean;
|
||||
roll: boolean;
|
||||
ref: boolean;
|
||||
type: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CsvColumns {
|
||||
[key: string]: ColumnOpts;
|
||||
}
|
||||
|
||||
function modFilter(mod: ParsedModifier, keep: ColumnOpts["keep"]) {
|
||||
if (
|
||||
keep.explicit &&
|
||||
(mod.info.generation === "suffix" ||
|
||||
mod.info.generation === "prefix" ||
|
||||
mod.info.generation === "mutated" ||
|
||||
// Should be redundant with prefix/suffix
|
||||
mod.info.type === ModifierType.Desecrated ||
|
||||
mod.info.type === ModifierType.Fractured)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (keep.implicit && mod.info.type === ModifierType.Implicit) {
|
||||
return true;
|
||||
}
|
||||
if (keep.enchant && mod.info.type === ModifierType.Enchant) {
|
||||
return true;
|
||||
}
|
||||
if (keep.augment && mod.info.type === ModifierType.Augment) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildCsvString(
|
||||
item: ParsedItem,
|
||||
sessionType: "chaos",
|
||||
addedMods: ParsedModifier[],
|
||||
removedMods: ParsedModifier[],
|
||||
opts: { columnOpts: ColumnOpts },
|
||||
): Result<string, string> {
|
||||
const { columnOpts } = opts;
|
||||
if (sessionType === "chaos") {
|
||||
const filteredMods = item.newMods.filter((mod) =>
|
||||
modFilter(mod, columnOpts.keep),
|
||||
);
|
||||
const out: Array<string | number | undefined> = [];
|
||||
if (columnOpts.refName) {
|
||||
out.push(item.info.refName);
|
||||
}
|
||||
if (columnOpts.itemLevel) {
|
||||
out.push(item.itemLevel);
|
||||
}
|
||||
if (columnOpts.rarity) {
|
||||
out.push(item.rarity);
|
||||
}
|
||||
if (columnOpts.sockets) {
|
||||
out.push(item.augmentSockets?.current ?? 0);
|
||||
}
|
||||
|
||||
if (columnOpts.mods) {
|
||||
out.push(
|
||||
arrayToCsvString(filteredMods.map((m) => modToShortMod(m, columnOpts))),
|
||||
);
|
||||
}
|
||||
|
||||
if (columnOpts.addedMods) {
|
||||
out.push(
|
||||
arrayToCsvString(
|
||||
addedMods
|
||||
.filter((mod) => modFilter(mod, columnOpts.keep))
|
||||
.map((m) => modToShortMod(m, columnOpts)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (columnOpts.removedMods) {
|
||||
out.push(
|
||||
arrayToCsvString(
|
||||
removedMods
|
||||
.filter((mod) => modFilter(mod, columnOpts.keep))
|
||||
.map((m) => modToShortMod(m, columnOpts)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ok(out.join(","));
|
||||
}
|
||||
|
||||
return err("sessionType not supported");
|
||||
}
|
||||
|
||||
export function getHeader(sessionType: "chaos") {
|
||||
switch (sessionType) {
|
||||
case "chaos":
|
||||
return ok("base,ilvl,rarity,sockets,mods,addedMods,removedMods");
|
||||
|
||||
default:
|
||||
return err("sessionType not supported");
|
||||
}
|
||||
}
|
||||
|
||||
export function diffItem(curr: ParsedItem, prev: ParsedItem | null) {
|
||||
if (!prev) {
|
||||
return {
|
||||
added: curr.newMods.filter(
|
||||
(mod) =>
|
||||
mod.info.generation === "suffix" || mod.info.generation === "prefix",
|
||||
),
|
||||
removed: [],
|
||||
};
|
||||
}
|
||||
|
||||
const filteredModsA = curr.newMods.filter(
|
||||
(mod) =>
|
||||
mod.info.generation === "suffix" || mod.info.generation === "prefix",
|
||||
);
|
||||
const filteredModsB = prev.newMods.filter(
|
||||
(mod) =>
|
||||
mod.info.generation === "suffix" || mod.info.generation === "prefix",
|
||||
);
|
||||
|
||||
const added = filteredModsA.filter(
|
||||
(mod) => !filteredModsB.some((modB) => modsEqual(mod, modB)),
|
||||
);
|
||||
const removed = filteredModsB.filter(
|
||||
(mod) => !filteredModsA.some((modA) => modsEqual(mod, modA)),
|
||||
);
|
||||
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
function modToShortMod(mod: ParsedModifier, opts: ColumnOpts): ShortMod {
|
||||
let type: ShortMod["type"];
|
||||
if (
|
||||
mod.info.generation &&
|
||||
mod.info.generation !== "suffix" &&
|
||||
mod.info.generation !== "prefix"
|
||||
) {
|
||||
type = mod.info.generation;
|
||||
} else {
|
||||
type = mod.info.type;
|
||||
}
|
||||
|
||||
return {
|
||||
name: mod.info.name,
|
||||
tier: mod.info.tier,
|
||||
roll: mod.stats.map((s) => s.roll?.value ?? -999),
|
||||
ref: mod.stats.map((s) => s.stat.ref),
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
function arrayToCsvString(arr: ShortMod[]) {
|
||||
const json = JSON.stringify(arr);
|
||||
return `"${json.replaceAll("'", "\\'").replaceAll('"', "'")}"`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const __testExports = {
|
||||
modFilter,
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import WidgetItemSearch from "../item-search/WidgetItemSearch.vue";
|
||||
import WidgetSettings from "../settings/SettingsWindow.vue";
|
||||
import WidgetXpTracker from "../leveling/WidgetXpTracker.vue";
|
||||
import WidgetNotepad from "../notepad/WidgetNotepad.vue";
|
||||
import WidgetLibrary from "../library/WidgetLibrary.vue";
|
||||
|
||||
type WidgetComponent = Component & { widget: WidgetSpec };
|
||||
|
||||
@@ -35,3 +36,4 @@ registry.widgets.push(WidgetItemCheck as unknown as WidgetComponent);
|
||||
registry.widgets.push(WidgetImageStrip as unknown as WidgetComponent);
|
||||
registry.widgets.push(WidgetDelveGrid as unknown as WidgetComponent);
|
||||
registry.widgets.push(WidgetNotepad as unknown as WidgetComponent);
|
||||
registry.widgets.push(WidgetLibrary as unknown as WidgetComponent);
|
||||
|
||||
@@ -141,6 +141,7 @@ import SettingsStashSearch from "../stash-search/stash-search-editor.vue";
|
||||
import SettingsStopwatch from "../stopwatch/settings-stopwatch.vue";
|
||||
import SettingsItemSearch from "../item-search/settings-item-search.vue";
|
||||
import SettingsLeveling from "../leveling/settings-leveling.vue";
|
||||
import SettingsLibrary from "../library/settings-library.vue";
|
||||
import { disableWidget, enableWidget, findWidget } from "./utils";
|
||||
|
||||
function shuffle<T>(array: T[]): T[] {
|
||||
@@ -260,6 +261,23 @@ export default defineComponent({
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() =>
|
||||
configClone.value?.enableAlphas &&
|
||||
configClone.value?.alphas.includes("library"),
|
||||
(curr) => {
|
||||
if (curr === undefined) return;
|
||||
const library = findWidget("library", configClone.value!);
|
||||
if (!library) return;
|
||||
|
||||
if (curr) {
|
||||
enableWidget(library);
|
||||
} else {
|
||||
disableWidget(library);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const menuItems = computed(() =>
|
||||
flatJoin(
|
||||
menuByType(configWidget.value?.wmType).map((group) =>
|
||||
@@ -329,6 +347,8 @@ function menuByType(type?: string) {
|
||||
return [[SettingsPricecheck]];
|
||||
case "item-search":
|
||||
return [[SettingsItemSearch]];
|
||||
case "library":
|
||||
return [[SettingsLibrary]];
|
||||
default:
|
||||
return [
|
||||
[SettingsHotkeys, SettingsChat],
|
||||
|
||||
@@ -97,26 +97,25 @@
|
||||
class="rounded bg-gray-900 px-1 block w-full mb-1 font-poe"
|
||||
/>
|
||||
</div>
|
||||
<ui-checkbox class="mb-4" v-model="readClientLog">{{
|
||||
<ui-checkbox class="mb-1" v-model="readClientLog">{{
|
||||
t(":read_client_log")
|
||||
}}</ui-checkbox>
|
||||
<div class="italic text-gray-500">
|
||||
<div class="italic text-gray-500 mb-4">
|
||||
{{ t(":client_log_explain") }}
|
||||
</div>
|
||||
<div class="mb-4" :class="{ 'p-2 bg-orange-800 rounded': enableAlphas }">
|
||||
<div class="mb-4" :class="{ 'p-2 bg-slate-800 rounded': enableAlphas }">
|
||||
<ui-checkbox class="mb-4" v-model="enableAlphas">{{
|
||||
t(":enable_alphas")
|
||||
}}</ui-checkbox>
|
||||
<div v-if="enableAlphas" class="mt-1">No alphas available right now</div>
|
||||
<div v-if="enableAlphas" class="mt-1">
|
||||
{{ t(":alphas_warning") }}
|
||||
</div>
|
||||
<ui-checkbox v-model="libraryAlpha" v-if="enableAlphas">{{
|
||||
t(":alpha_library")
|
||||
}}</ui-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from "vue";
|
||||
import { defineComponent, computed, ref, watch } from "vue";
|
||||
import { useI18nNs } from "@/web/i18n";
|
||||
import UiRadio from "@/web/ui/UiRadio.vue";
|
||||
import UiCheckbox from "@/web/ui/UiCheckbox.vue";
|
||||
@@ -129,6 +128,22 @@ export default defineComponent({
|
||||
props: configProp(),
|
||||
setup(props) {
|
||||
const { t } = useI18nNs("settings");
|
||||
const libraryAlpha = ref(
|
||||
AppConfig().enableAlphas && AppConfig().alphas.includes("library"),
|
||||
);
|
||||
watch(
|
||||
libraryAlpha,
|
||||
(value) => {
|
||||
if (value) {
|
||||
props.config.alphas.push("library");
|
||||
} else {
|
||||
props.config.alphas = props.config.alphas.filter(
|
||||
(alpha) => alpha !== "library",
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
t,
|
||||
@@ -203,6 +218,7 @@ export default defineComponent({
|
||||
windowTitle: configModelValue(() => props.config, "windowTitle"),
|
||||
enableAlphas: configModelValue(() => props.config, "enableAlphas"),
|
||||
readClientLog: configModelValue(() => props.config, "readClientLog"),
|
||||
libraryAlpha,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -149,6 +149,17 @@ module.exports = {
|
||||
800: "#86198f",
|
||||
900: "#701a75",
|
||||
},
|
||||
slate: {
|
||||
100: "#f1f5f9",
|
||||
200: "#e2e8f0",
|
||||
300: "#cbd5e1",
|
||||
400: "#94a3b8",
|
||||
500: "#64748b",
|
||||
600: "#475569",
|
||||
700: "#334155",
|
||||
800: "#1e293b",
|
||||
900: "#0f172a",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
|
||||
Reference in New Issue
Block a user