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:
Kvan7
2026-03-27 18:41:04 -05:00
committed by GitHub
parent 6dfc44117f
commit 4e9dc7fcb2
18 changed files with 960 additions and 20 deletions
+19 -2
View File
@@ -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;
+87
View File
@@ -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");
}
}
+3
View File
@@ -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();
-2
View File
@@ -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?",
+10 -2
View File
@@ -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:"
}
}
+1 -3
View File
@@ -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",
-2
View File
@@ -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));
}
},
);
});
+17
View File
@@ -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",
+28 -1
View File
@@ -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>
+251
View File
@@ -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>
+199
View File
@@ -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],
+24 -8
View File
@@ -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,
};
},
});
+11
View File
@@ -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: {},