Compare commits

..

7 Commits

Author SHA1 Message Date
kvan7
df966c8041 Style updates 2026-01-16 16:44:20 -06:00
kvan7
c0fa40bb74 ui responsive 2026-01-15 21:51:11 -06:00
kvan7
f5615ef391 Adds writing csv 2026-01-15 20:23:17 -06:00
kvan7
3af6d48934 updating config stuff 2026-01-15 17:15:39 -06:00
kvan7
2d342bd86a Add hotkey action 2026-01-15 15:12:15 -06:00
kvan7
7b0faf9f3f add as alpha 2026-01-15 13:19:45 -06:00
kvan7
3fc3e13ac5 create base component 2026-01-15 11:43:25 -06:00
15 changed files with 498 additions and 17 deletions

View File

@@ -9,6 +9,7 @@ export interface HostConfig {
windowTitle: string;
language: string;
readClientLog: boolean;
libraryAlpha: boolean;
}
export interface ShortcutAction {
@@ -88,7 +89,8 @@ export type IpcEvent =
| IpcItemText
| IpcOcrText
| IpcConfigChanged
| IpcUserAction;
| IpcUserAction
| IpcWriteCsv;
export type IpcEventPayload<
Name extends IpcEvent["name"],
@@ -209,6 +211,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;

View File

@@ -0,0 +1,79 @@
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 uploadsPath = path.join(
app.getPath("userData"),
"apt-data",
"csv-data",
);
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) {
this._enabled = enabled;
}
private async writeSessionStart(name: string, header: string) {
try {
if (!existsSync(this.uploadsPath)) {
await fs.mkdir(this.uploadsPath, { recursive: true });
}
const file = await fs.open(
path.join(this.uploadsPath, name + ".csv"),
"w",
);
this._state = { file };
this.writeLine(header);
} 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");
}
}

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);
},
);
uIOhook.start();

View File

@@ -330,8 +330,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?",

View File

@@ -362,8 +362,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",
@@ -467,5 +467,12 @@
"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_file_help": "Some info text",
"record_count": "Session Rolls:"
}
}

View File

@@ -328,9 +328,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",

View File

@@ -350,8 +350,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",

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,6 @@ function getConfigForHost(): HostConfig {
windowTitle: config.windowTitle,
language: config.language,
readClientLog: config.readClientLog,
libraryAlpha: config.enableAlphas && config.alphas.includes("library"),
};
}

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>
<div class="flex flex-col gap-y-1 overflow-y-auto min-h-0">
<div :class="$style.dataField">
<div>{{ t(":record_count") }}</div>
<div
class="text-center bg-transparent text-gray-300 border-2 rounded border-gray-900 p-1"
>
{{ rollCount }}
</div>
</div>
<div :class="$style.dataField">
<div>{{ lastMod }}</div>
</div>
</div>
</div>
</Widget>
</template>
<script lang="ts">
import {
computed,
defineComponent,
PropType,
ref,
shallowRef,
watch,
} from "vue";
import Widget from "../overlay/Widget.vue";
import { WidgetSpec } from "../overlay/interfaces";
import { LibraryWidget } from "./widget";
import { useI18n } from "vue-i18n";
import { Host, MainProcess } from "../background/IPC";
import { parseClipboard, ParsedItem } from "@/parser";
import { AppConfig } from "../Config";
import { err, ok, Result } from "neverthrow";
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,
},
});
}
function buildCsvString(
item: ParsedItem,
sessionType: "chaos",
): Result<string, string> {
if (sessionType === "chaos") {
return ok(
[
item.info.refName,
item.itemLevel,
`"${JSON.stringify(item.newMods.map((mod) => mod.info.name))}"`,
`"${JSON.stringify(item.newMods.map((mod) => mod.info.tier))}"`,
].join(","),
);
}
return err("sessionType not supported");
}
const headers = {
chaos: "base,ilvl,mod,tier",
};
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,
outputPath: null,
};
},
} satisfies WidgetSpec,
components: { Widget },
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 item = ref<ParsedItem | null>(null);
const rollCount = shallowRef<number>(0);
const sessionType = shallowRef<"chaos">("chaos");
watch(libEnabled, (curr) => {
if (!curr) {
endSessionHost();
inSession.value = false;
}
});
function startSession() {
item.value = null;
rollCount.value = 0;
startSessionHost(sessionName.value, headers[sessionType.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");
}
}
watch(
item,
(curr) => {
if (!curr) return;
buildCsvString(curr, sessionType.value)
.andThen((text) => {
Host.sendEvent({
name: "CLIENT->MAIN::write-data",
payload: {
action: "log-item",
text,
},
});
rollCount.value++;
return ok(null);
})
.mapErr((err) => {
console.warn(err);
});
},
{ deep: true },
);
MainProcess.onEvent("MAIN->CLIENT::item-text", (e) => {
if (e.target !== "log-item") return;
if (!libEnabled.value) return;
if (!inSession.value) return;
performance.mark("log-item-event");
item.value = parseClipboard(e.clipboard).unwrapOr(null);
performance.mark("log-item-parsed");
});
const { t } = useI18n();
return {
t,
startSession,
endSession,
inSession,
sessionName,
rollCount,
lastMod: computed(() => item.value?.newMods.find(() => true)?.info.name),
};
},
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 flex flex-row justify-between;
@apply content-center;
text-align: left;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@@ -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-4">
<div class="flex-1 mb-1">{{ t(":poe_log_file") }}</div>
<input
v-model.trim="outputPath"
class="rounded bg-gray-900 px-1 block w-full font-sans"
placeholder="...?/Documents/My Games/Path of Exile 2/output.csv"
/>
</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",
),
outputPath: configModelValue(
() => configLibraryWidget.value,
"outputPath",
),
};
},
});
</script>

View File

@@ -0,0 +1,7 @@
import { Anchor, Widget } from "../overlay/widgets";
export interface LibraryWidget extends Widget {
anchor: Anchor;
logItemKey: string | null;
outputPath: string | null;
}

View File

@@ -10,6 +10,7 @@ import WidgetDelveGrid from "./WidgetDelveGrid.vue";
import WidgetItemSearch from "../item-search/WidgetItemSearch.vue";
import WidgetSettings from "../settings/SettingsWindow.vue";
import WidgetXpTracker from "../leveling/WidgetXpTracker.vue";
import WidgetLibrary from "../library/WidgetLibrary.vue";
type WidgetComponent = Component & { widget: WidgetSpec };
@@ -33,3 +34,4 @@ registry.widgets.push(PriceCheckWindow as unknown as WidgetComponent);
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(WidgetLibrary as unknown as WidgetComponent);

View File

@@ -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],

View File

@@ -103,20 +103,19 @@
<div class="italic text-gray-500">
{{ 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,
};
},
});

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: {},