From cbff3f9ad97e7d75891bf6960e0c5ae2e112ab83 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sat, 28 Feb 2026 21:15:22 -0800 Subject: [PATCH] run prettier --- AGENTS.md | 2 +- README.md | 1 - config.example.jsonc | 82 +++++-------- launcher/aniskip-metadata.test.ts | 2 +- launcher/aniskip-metadata.ts | 12 +- launcher/config.test.ts | 8 +- launcher/config/cli-parser-builder.ts | 3 +- launcher/main.test.ts | 17 +-- launcher/mpv.ts | 3 +- launcher/parse-args.test.ts | 6 +- src/anki-integration.ts | 3 +- .../anki-connect-proxy.test.ts | 108 ++++++++++-------- src/anki-integration/anki-connect-proxy.ts | 4 +- src/anki-integration/duplicate.test.ts | 6 +- src/anki-integration/duplicate.ts | 4 +- .../field-grouping-workflow.ts | 7 +- .../note-update-workflow.test.ts | 16 +-- src/config/config.test.ts | 3 + .../definitions/domain-registry.test.ts | 4 +- .../definitions/options-integrations.ts | 6 +- src/config/definitions/runtime-options.ts | 36 ++++++ src/config/resolve/subtitle-domains.ts | 16 ++- src/config/template.ts | 3 +- .../services/anilist/anilist-token-store.ts | 9 +- src/core/services/cli-command.ts | 3 +- .../services/frequency-dictionary.test.ts | 3 +- src/core/services/frequency-dictionary.ts | 2 +- src/core/services/index.ts | 5 +- src/core/services/ipc.test.ts | 11 +- src/core/services/jellyfin-token-store.ts | 7 +- src/core/services/overlay-bridge.ts | 5 +- src/core/services/overlay-drop.ts | 4 +- src/core/services/overlay-manager.test.ts | 10 +- .../services/overlay-runtime-init.test.ts | 26 ++--- src/core/services/overlay-runtime-init.ts | 8 +- .../subtitle-processing-controller.test.ts | 32 ++++++ .../subtitle-processing-controller.ts | 4 + .../tokenizer/annotation-stage.test.ts | 7 +- .../services/tokenizer/annotation-stage.ts | 30 ++--- .../parser-enrichment-worker-runtime.ts | 4 +- src/main.ts | 10 +- src/main/overlay-runtime.test.ts | 30 +++-- .../runtime/anilist-media-guess-main-deps.ts | 14 ++- .../runtime/anilist-media-state-main-deps.ts | 8 +- .../runtime/anilist-post-watch-main-deps.ts | 3 +- src/main/runtime/anilist-post-watch.ts | 12 +- .../anilist-setup-protocol-main-deps.ts | 3 +- src/main/runtime/anilist-setup-protocol.ts | 6 +- src/main/runtime/anilist-setup-window.test.ts | 10 +- src/main/runtime/anilist-setup-window.ts | 19 +-- src/main/runtime/anilist-setup.ts | 4 +- src/main/runtime/anilist-token-refresh.ts | 4 +- src/main/runtime/anki-actions-main-deps.ts | 16 ++- .../runtime/app-lifecycle-main-cleanup.ts | 3 +- src/main/runtime/app-ready-main-deps.ts | 7 +- .../runtime/app-runtime-main-deps.test.ts | 3 +- .../runtime/cli-command-context-factory.ts | 4 +- src/main/runtime/cli-command-context.test.ts | 4 +- .../runtime/cli-command-runtime-handler.ts | 11 +- src/main/runtime/clipboard-queue.ts | 7 +- .../jellyfin-remote-composer.test.ts | 3 +- .../runtime/composers/mpv-runtime-composer.ts | 2 +- src/main/runtime/config-derived.ts | 5 +- .../config-hot-reload-main-deps.test.ts | 9 +- .../runtime/config-hot-reload-main-deps.ts | 7 +- .../dictionary-runtime-main-deps.test.ts | 3 +- .../global-shortcuts-main-deps.test.ts | 10 +- .../global-shortcuts-runtime-handlers.ts | 10 +- src/main/runtime/global-shortcuts.ts | 5 +- .../immersion-startup-main-deps.test.ts | 11 +- src/main/runtime/initial-args-handler.test.ts | 6 +- .../initial-args-runtime-handler.test.ts | 9 +- src/main/runtime/ipc-mpv-command-main-deps.ts | 4 +- src/main/runtime/ipc-runtime-handlers.ts | 13 ++- src/main/runtime/jellyfin-cli-list.ts | 6 +- src/main/runtime/jellyfin-cli-main-deps.ts | 16 +-- .../runtime/jellyfin-client-info-main-deps.ts | 4 +- src/main/runtime/jellyfin-client-info.test.ts | 7 +- .../jellyfin-playback-launch-main-deps.ts | 4 +- .../runtime/jellyfin-remote-commands.test.ts | 4 +- src/main/runtime/jellyfin-remote-playback.ts | 4 +- .../jellyfin-remote-session-main-deps.ts | 8 +- .../jellyfin-setup-window-main-deps.test.ts | 31 +++-- .../runtime/jellyfin-setup-window.test.ts | 36 ++++-- src/main/runtime/jellyfin-setup-window.ts | 20 ++-- src/main/runtime/jellyfin-subtitle-preload.ts | 8 +- src/main/runtime/mining-actions.ts | 43 +++---- ...v-client-runtime-service-main-deps.test.ts | 5 +- .../mpv-client-runtime-service-main-deps.ts | 3 +- .../mpv-jellyfin-defaults-main-deps.ts | 4 +- src/main/runtime/mpv-main-event-bindings.ts | 3 +- .../mpv-subtitle-render-metrics-main-deps.ts | 4 +- ...numeric-shortcut-runtime-main-deps.test.ts | 5 +- .../numeric-shortcut-runtime-main-deps.ts | 4 +- .../numeric-shortcut-session-main-deps.ts | 8 +- ...meric-shortcut-session-runtime-handlers.ts | 10 +- .../runtime/overlay-bootstrap-main-deps.ts | 4 +- .../overlay-main-actions-main-deps.test.ts | 15 ++- .../runtime/overlay-main-actions-main-deps.ts | 7 +- src/main/runtime/overlay-main-actions.ts | 11 +- .../runtime/overlay-mpv-sub-visibility.ts | 1 - .../runtime/overlay-runtime-bootstrap.test.ts | 2 +- ...lay-runtime-main-actions-main-deps.test.ts | 5 +- .../overlay-runtime-main-actions-main-deps.ts | 14 +-- .../overlay-runtime-main-actions.test.ts | 13 ++- .../runtime/overlay-runtime-main-actions.ts | 6 +- .../overlay-shortcuts-lifecycle-main-deps.ts | 4 +- .../overlay-shortcuts-runtime-handlers.ts | 26 +++-- ...verlay-shortcuts-runtime-main-deps.test.ts | 2 +- .../overlay-visibility-actions-main-deps.ts | 3 +- .../overlay-visibility-actions.test.ts | 6 +- .../overlay-visibility-runtime-main-deps.ts | 3 +- .../overlay-window-layout-main-deps.test.ts | 7 +- .../overlay-window-layout-main-deps.ts | 4 +- .../runtime-bootstrap-main-deps.test.ts | 10 +- .../secondary-sub-mode-main-deps.test.ts | 3 +- ...secondary-sub-mode-runtime-handler.test.ts | 6 +- .../secondary-sub-mode-runtime-handler.ts | 5 +- src/main/runtime/startup-config.test.ts | 4 +- src/main/runtime/startup-runtime-handlers.ts | 5 +- src/main/runtime/startup-warmups-main-deps.ts | 8 +- src/main/runtime/subsync-runtime.ts | 12 +- .../subtitle-position-main-deps.test.ts | 2 +- .../subtitle-tokenization-main-deps.test.ts | 5 +- .../subtitle-tokenization-main-deps.ts | 9 +- src/main/runtime/tray-lifecycle.test.ts | 6 +- src/main/runtime/tray-lifecycle.ts | 6 +- .../runtime/tray-runtime-handlers.test.ts | 4 +- .../yomitan-extension-loader-main-deps.ts | 4 +- src/main/runtime/yomitan-extension-runtime.ts | 5 +- src/main/runtime/yomitan-settings-runtime.ts | 4 +- src/preload.ts | 13 +-- src/renderer/error-recovery.test.ts | 30 +++++ src/renderer/handlers/keyboard.ts | 8 +- src/renderer/handlers/mouse.ts | 5 +- src/renderer/modals/runtime-options.ts | 16 +++ src/renderer/positioning/controller.ts | 6 +- src/renderer/renderer.ts | 5 +- src/renderer/style.css | 3 +- src/renderer/subtitle-render.test.ts | 50 +++++--- src/renderer/subtitle-render.ts | 25 ++-- src/runtime-options.ts | 12 +- src/shared/ipc/validators.ts | 3 + src/token-merger.ts | 25 ++-- src/token-pos2-exclusions.ts | 4 +- src/types.ts | 5 +- 146 files changed, 891 insertions(+), 584 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b905b00..1b3f6b5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,3 @@ - @@ -17,6 +16,7 @@ This project uses Backlog.md MCP for all task and project management activities. - **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work These guides cover: + - Decision framework for when to create tasks - Search-first workflow to avoid duplicates - Links to detailed guides for task creation, execution, and finalization diff --git a/README.md b/README.md index b4854aa..7035b50 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ cp /tmp/plugin/subminer.conf ~/.config/mpv/script-opts/ mkdir -p ~/.config/SubMiner && cp /tmp/config.example.jsonc ~/.config/SubMiner/config.jsonc ``` - ### 3. Set up Yomitan Dictionaries ```bash diff --git a/config.example.jsonc b/config.example.jsonc index fb67f02..4901676 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -5,7 +5,6 @@ * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed. */ { - // ========================================== // Overlay Auto-Start // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. @@ -17,7 +16,7 @@ // Control whether browser opens automatically for texthooker. // ========================================== "texthooker": { - "openBrowser": true // Open browser setting. Values: true | false + "openBrowser": true, // Open browser setting. Values: true | false }, // Control whether browser opens automatically for texthooker. // ========================================== @@ -27,7 +26,7 @@ // ========================================== "websocket": { "enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false - "port": 6677 // Built-in subtitle websocket server port. + "port": 6677, // Built-in subtitle websocket server port. }, // Built-in WebSocket server broadcasts subtitle text to connected clients. // ========================================== @@ -36,7 +35,7 @@ // Set to debug for full runtime diagnostics. // ========================================== "logging": { - "level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error + "level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error }, // Controls logging verbosity. Keep this as an object; do not replace with a bare string. // ========================================== @@ -57,7 +56,7 @@ "toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. - "openJimaku": "Ctrl+Shift+J" // Open jimaku setting. + "openJimaku": "Ctrl+Shift+J", // Open jimaku setting. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable. // ========================================== @@ -77,7 +76,7 @@ "secondarySub": { "secondarySubLanguages": [], // Secondary sub languages setting. "autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false - "defaultMode": "hover" // Default mode setting. + "defaultMode": "hover", // Default mode setting. }, // Dual subtitle track options. // ========================================== @@ -88,7 +87,7 @@ "defaultMode": "auto", // Subsync default mode. Values: auto | manual "alass_path": "", // Alass path setting. "ffsubsync_path": "", // Ffsubsync path setting. - "ffmpeg_path": "" // Ffmpeg path setting. + "ffmpeg_path": "", // Ffmpeg path setting. }, // Subsync engine and executable paths. // ========================================== @@ -96,7 +95,7 @@ // Initial vertical subtitle position from the bottom. // ========================================== "subtitlePosition": { - "yPercent": 10 // Y percent setting. + "yPercent": 10, // Y percent setting. }, // Initial vertical subtitle position from the bottom. // ========================================== @@ -129,7 +128,7 @@ "N2": "#f5a97f", // N2 setting. "N3": "#f9e2af", // N3 setting. "N4": "#a6e3a1", // N4 setting. - "N5": "#8aadf4" // N5 setting. + "N5": "#8aadf4", // N5 setting. }, // Jlpt colors setting. "frequencyDictionary": { "enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false @@ -138,13 +137,7 @@ "mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded "matchMode": "headword", // Frequency lookup text selection mode. Values: headword | surface "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`. - "bandedColors": [ - "#ed8796", - "#f5a97f", - "#f9e2af", - "#a6e3a1", - "#8aadf4" - ] // Five colors used for rank bands when mode is `banded` (from most common to least within topX). + "bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX). }, // Frequency dictionary setting. "secondary": { "fontFamily": "Manrope, Inter", // Font family setting. @@ -159,8 +152,8 @@ "backgroundColor": "transparent", // Background color setting. "backdropFilter": "blur(6px)", // Backdrop filter setting. "fontWeight": "normal", // Font weight setting. - "fontStyle": "normal" // Font style setting. - } // Secondary setting. + "fontStyle": "normal", // Font style setting. + }, // Secondary setting. }, // Primary and secondary subtitle styling. // ========================================== @@ -177,17 +170,15 @@ "enabled": false, // Enable local AnkiConnect-compatible proxy for push-based auto-enrichment. Values: true | false "host": "127.0.0.1", // Bind host for local AnkiConnect proxy. "port": 8766, // Bind port for local AnkiConnect proxy. - "upstreamUrl": "http://127.0.0.1:8765" // Upstream AnkiConnect URL proxied by local AnkiConnect proxy. + "upstreamUrl": "http://127.0.0.1:8765", // Upstream AnkiConnect URL proxied by local AnkiConnect proxy. }, // Proxy setting. - "tags": [ - "SubMiner" - ], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. + "tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. "fields": { "audio": "ExpressionAudio", // Audio setting. "image": "Picture", // Image setting. "sentence": "Sentence", // Sentence setting. "miscInfo": "MiscInfo", // Misc info setting. - "translation": "SelectionText" // Translation setting. + "translation": "SelectionText", // Translation setting. }, // Fields setting. "ai": { "enabled": false, // Enabled setting. Values: true | false @@ -196,7 +187,7 @@ "model": "openai/gpt-4o-mini", // Model setting. "baseUrl": "https://openrouter.ai/api", // Base url setting. "targetLanguage": "English", // Target language setting. - "systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting. + "systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting. }, // Ai setting. "media": { "generateAudio": true, // Generate audio setting. Values: true | false @@ -209,7 +200,7 @@ "animatedCrf": 35, // Animated crf setting. "audioPadding": 0.5, // Audio padding setting. "fallbackDuration": 3, // Fallback duration setting. - "maxMediaDuration": 30 // Max media duration setting. + "maxMediaDuration": 30, // Max media duration setting. }, // Media setting. "behavior": { "overwriteAudio": true, // Overwrite audio setting. Values: true | false @@ -217,7 +208,7 @@ "mediaInsertMode": "append", // Media insert mode setting. "highlightWord": true, // Highlight word setting. Values: true | false "notificationType": "osd", // Notification type setting. - "autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false + "autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false }, // Behavior setting. "nPlusOne": { "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false @@ -226,20 +217,20 @@ "decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names. "minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight. - "knownWord": "#a6da95" // Color used for legacy known-word highlights. + "knownWord": "#a6da95", // Color used for legacy known-word highlights. }, // N plus one setting. "metadata": { - "pattern": "[SubMiner] %f (%t)" // Pattern setting. + "pattern": "[SubMiner] %f (%t)", // Pattern setting. }, // Metadata setting. "isLapis": { "enabled": false, // Enabled setting. Values: true | false - "sentenceCardModel": "Japanese sentences" // Sentence card model setting. + "sentenceCardModel": "Japanese sentences", // Sentence card model setting. }, // Is lapis setting. "isKiku": { "enabled": false, // Enabled setting. Values: true | false "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled - "deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false - } // Is kiku setting. + "deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false + }, // Is kiku setting. }, // Automatic Anki updates and media generation options. // ========================================== @@ -249,7 +240,7 @@ "jimaku": { "apiBaseUrl": "https://jimaku.cc", // Api base url setting. "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none - "maxEntryResults": 10 // Maximum Jimaku search results returned. + "maxEntryResults": 10, // Maximum Jimaku search results returned. }, // Jimaku API configuration and defaults. // ========================================== @@ -260,10 +251,7 @@ "mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off "whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine. "whisperModel": "", // Path to whisper model used for fallback transcription. - "primarySubLanguages": [ - "ja", - "jpn" - ] // Comma-separated primary subtitle language priority used by the launcher. + "primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher. }, // Defaults for subminer YouTube subtitle extraction/transcription mode. // ========================================== @@ -272,7 +260,7 @@ // ========================================== "anilist": { "enabled": false, // Enable AniList post-watch progress updates. Values: true | false - "accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup. + "accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup. }, // Anilist API credentials and update behavior. // ========================================== @@ -296,16 +284,8 @@ "pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false "iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons. "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false - "directPlayContainers": [ - "mkv", - "mp4", - "webm", - "mov", - "flac", - "mp3", - "aac" - ], // Container allowlist for direct play decisions. - "transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable. + "directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions. + "transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable. }, // Optional Jellyfin integration for auth, browsing, and playback launch. // ========================================== @@ -316,7 +296,7 @@ "discordPresence": { "enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false "updateIntervalMs": 3000, // Minimum interval between presence payload updates. - "debounceMs": 750 // Debounce delay used to collapse bursty presence updates. + "debounceMs": 750, // Debounce delay used to collapse bursty presence updates. }, // Optional Discord Rich Presence activity card updates for current playback/study session. // ========================================== @@ -338,7 +318,7 @@ "telemetryDays": 30, // Telemetry retention window in days. "dailyRollupsDays": 365, // Daily rollup retention window in days. "monthlyRollupsDays": 1825, // Monthly rollup retention window in days. - "vacuumIntervalDays": 7 // Minimum days between VACUUM runs. - } // Retention setting. - } // Enable/disable immersion tracking. + "vacuumIntervalDays": 7, // Minimum days between VACUUM runs. + }, // Retention setting. + }, // Enable/disable immersion tracking. } diff --git a/launcher/aniskip-metadata.test.ts b/launcher/aniskip-metadata.test.ts index 0404947..19b5649 100644 --- a/launcher/aniskip-metadata.test.ts +++ b/launcher/aniskip-metadata.test.ts @@ -62,7 +62,7 @@ test('inferAniSkipMetadataForFile falls back to anime directory title when filen test('buildSubminerScriptOpts includes aniskip metadata fields', () => { const opts = buildSubminerScriptOpts('/tmp/SubMiner.AppImage', '/tmp/subminer.sock', { - title: 'Frieren: Beyond Journey\'s End', + title: "Frieren: Beyond Journey's End", season: 1, episode: 5, source: 'guessit', diff --git a/launcher/aniskip-metadata.ts b/launcher/aniskip-metadata.ts index ce1a8cc..686aed6 100644 --- a/launcher/aniskip-metadata.ts +++ b/launcher/aniskip-metadata.ts @@ -28,7 +28,11 @@ function toPositiveInt(value: unknown): number | null { } function detectEpisodeFromName(baseName: string): number | null { - const patterns = [/[Ss]\d+[Ee](\d{1,3})/, /(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/, /[-\s](\d{1,3})$/]; + const patterns = [ + /[Ss]\d+[Ee](\d{1,3})/, + /(?:^|[\s._-])[Ee][Pp]?[\s._-]*(\d{1,3})(?:$|[\s._-])/, + /[-\s](\d{1,3})$/, + ]; for (const pattern of patterns) { const match = baseName.match(pattern); if (!match || !match[1]) continue; @@ -171,7 +175,11 @@ export function inferAniSkipMetadataForFile( } function sanitizeScriptOptValue(value: string): string { - return value.replace(/,/g, ' ').replace(/[\r\n]/g, ' ').replace(/\s+/g, ' ').trim(); + return value + .replace(/,/g, ' ') + .replace(/[\r\n]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); } export function buildSubminerScriptOpts( diff --git a/launcher/config.test.ts b/launcher/config.test.ts index a559df2..1007b51 100644 --- a/launcher/config.test.ts +++ b/launcher/config.test.ts @@ -4,11 +4,9 @@ import { execFileSync } from 'node:child_process'; import path from 'node:path'; test('launcher root help lists subcommands', () => { - const output = execFileSync( - 'bun', - ['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], - { encoding: 'utf8' }, - ); + const output = execFileSync('bun', ['run', path.join(process.cwd(), 'launcher/main.ts'), '-h'], { + encoding: 'utf8', + }); assert.match(output, /Commands:/); assert.match(output, /jellyfin\|jf/); diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index 99a7015..5874781 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -182,7 +182,8 @@ export function parseCliPrograms( server: typeof options.server === 'string' ? options.server : undefined, username: typeof options.username === 'string' ? options.username : undefined, password: typeof options.password === 'string' ? options.password : undefined, - passwordStore: typeof options.passwordStore === 'string' ? options.passwordStore : undefined, + passwordStore: + typeof options.passwordStore === 'string' ? options.passwordStore : undefined, logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined, }; }); diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 3cdfec1..0a98632 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -22,10 +22,14 @@ function withTempDir(fn: (dir: string) => T): T { } function runLauncher(argv: string[], env: NodeJS.ProcessEnv): RunResult { - const result = spawnSync(process.execPath, ['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv], { - env, - encoding: 'utf8', - }); + const result = spawnSync( + process.execPath, + ['run', path.join(process.cwd(), 'launcher/main.ts'), ...argv], + { + env, + encoding: 'utf8', + }, + ); return { status: result.status, stdout: result.stdout || '', @@ -225,10 +229,7 @@ test('jellyfin setup forwards password-store to app command', () => { SUBMINER_APPIMAGE_PATH: appPath, SUBMINER_TEST_CAPTURE: capturePath, }; - const result = runLauncher( - ['jf', 'setup', '--password-store', 'gnome-libsecret'], - env, - ); + const result = runLauncher(['jf', 'setup', '--password-store', 'gnome-libsecret'], env); assert.equal(result.status, 0); assert.equal( diff --git a/launcher/mpv.ts b/launcher/mpv.ts index 7848202..6622a16 100644 --- a/launcher/mpv.ts +++ b/launcher/mpv.ts @@ -475,8 +475,7 @@ export function startMpv( if (preloadedSubtitles?.secondaryPath) { mpvArgs.push(`--sub-file=${preloadedSubtitles.secondaryPath}`); } - const aniSkipMetadata = - targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null; + const aniSkipMetadata = targetKind === 'file' ? inferAniSkipMetadataForFile(target) : null; const scriptOpts = buildSubminerScriptOpts(appPath, socketPath, aniSkipMetadata); if (aniSkipMetadata) { log( diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index e35d8ed..c485da1 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -31,11 +31,7 @@ test('parseArgs maps jellyfin play action and log-level override', () => { }); test('parseArgs forwards jellyfin password-store option', () => { - const parsed = parseArgs( - ['jf', 'setup', '--password-store', 'gnome-libsecret'], - 'subminer', - {}, - ); + const parsed = parseArgs(['jf', 'setup', '--password-store', 'gnome-libsecret'], 'subminer', {}); assert.equal(parsed.jellyfin, true); assert.equal(parsed.passwordStore, 'gnome-libsecret'); diff --git a/src/anki-integration.ts b/src/anki-integration.ts index 6b46cdd..c03b2c5 100644 --- a/src/anki-integration.ts +++ b/src/anki-integration.ts @@ -239,7 +239,8 @@ export class AnkiIntegration { } private createProxyServer(): AnkiConnectProxyServer { - const { AnkiConnectProxyServer } = require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy'); + const { AnkiConnectProxyServer } = + require('./anki-integration/anki-connect-proxy') as typeof import('./anki-integration/anki-connect-proxy'); return new AnkiConnectProxyServer({ shouldAutoUpdateNewCards: () => this.config.behavior?.autoUpdateNewCards !== false, processNewCard: (noteId: number) => this.processNewCard(noteId), diff --git a/src/anki-integration/anki-connect-proxy.test.ts b/src/anki-integration/anki-connect-proxy.test.ts index c830921..6508019 100644 --- a/src/anki-integration/anki-connect-proxy.test.ts +++ b/src/anki-integration/anki-connect-proxy.test.ts @@ -27,9 +27,11 @@ test('proxy enqueues addNote result for enrichment', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'addNote' }, Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'), ); @@ -50,9 +52,11 @@ test('proxy enqueues addNote bare numeric response for enrichment', async () => logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8')); + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest({ action: 'addNote' }, Buffer.from('42', 'utf8')); await waitForCondition(() => processed.length === 1); assert.deepEqual(processed, [42]); @@ -71,9 +75,11 @@ test('proxy de-duplicates addNotes IDs within the same response', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'addNotes' }, Buffer.from(JSON.stringify({ result: [101, 102, 101, null], error: null }), 'utf8'), ); @@ -94,17 +100,15 @@ test('proxy enqueues note IDs from multi action addNote/addNotes results', async logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'multi', params: { - actions: [ - { action: 'version' }, - { action: 'addNote' }, - { action: 'addNotes' }, - ], + actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }], }, }, Buffer.from(JSON.stringify({ result: [6, 777, [888, 777, null]], error: null }), 'utf8'), @@ -126,9 +130,11 @@ test('proxy enqueues note IDs from bare multi action results', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'multi', params: { @@ -154,17 +160,15 @@ test('proxy enqueues note IDs from multi action envelope results', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'multi', params: { - actions: [ - { action: 'version' }, - { action: 'addNote' }, - { action: 'addNotes' }, - ], + actions: [{ action: 'version' }, { action: 'addNote' }, { action: 'addNotes' }], }, }, Buffer.from( @@ -196,9 +200,11 @@ test('proxy skips auto-enrichment when auto-update is disabled', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'addNote' }, Buffer.from(JSON.stringify({ result: 303, error: null }), 'utf8'), ); @@ -219,9 +225,11 @@ test('proxy ignores addNote when upstream response reports error', async () => { logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'addNote' }, Buffer.from(JSON.stringify({ result: 123, error: 'duplicate' }), 'utf8'), ); @@ -248,9 +256,11 @@ test('proxy does not fallback-enqueue latest note for multi requests without add logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'multi', params: { @@ -283,9 +293,11 @@ test('proxy fallback-enqueues latest note for addNote responses without note IDs logError: () => undefined, }); - (proxy as unknown as { - maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; - }).maybeEnqueueFromRequest( + ( + proxy as unknown as { + maybeEnqueueFromRequest: (request: Record, responseBody: Buffer) => void; + } + ).maybeEnqueueFromRequest( { action: 'addNote' }, Buffer.from(JSON.stringify({ result: 0, error: null }), 'utf8'), ); @@ -304,13 +316,15 @@ test('proxy detects self-referential loop configuration', () => { logError: () => undefined, }); - const result = (proxy as unknown as { - isSelfReferentialProxy: (options: { - host: string; - port: number; - upstreamUrl: string; - }) => boolean; - }).isSelfReferentialProxy({ + const result = ( + proxy as unknown as { + isSelfReferentialProxy: (options: { + host: string; + port: number; + upstreamUrl: string; + }) => boolean; + } + ).isSelfReferentialProxy({ host: '127.0.0.1', port: 8766, upstreamUrl: 'http://localhost:8766', diff --git a/src/anki-integration/anki-connect-proxy.ts b/src/anki-integration/anki-connect-proxy.ts index 685fa17..a39e8f2 100644 --- a/src/anki-integration/anki-connect-proxy.ts +++ b/src/anki-integration/anki-connect-proxy.ts @@ -175,7 +175,9 @@ export class AnkiConnectProxyServer { } const action = - typeof requestJson.action === 'string' ? requestJson.action : String(requestJson.action ?? ''); + typeof requestJson.action === 'string' + ? requestJson.action + : String(requestJson.action ?? ''); if (action !== 'addNote' && action !== 'addNotes' && action !== 'multi') { return; } diff --git a/src/anki-integration/duplicate.test.ts b/src/anki-integration/duplicate.test.ts index 240c6b2..5a258e9 100644 --- a/src/anki-integration/duplicate.test.ts +++ b/src/anki-integration/duplicate.test.ts @@ -89,7 +89,11 @@ test('findDuplicateNote checks both source expression/word values when both fiel if (query.includes('昨日は雨だった。')) { return []; } - if (query.includes('"Word:雨"') || query.includes('"word:雨"') || query.includes('"Expression:雨"')) { + if ( + query.includes('"Word:雨"') || + query.includes('"word:雨"') || + query.includes('"Expression:雨"') + ) { return [200]; } return []; diff --git a/src/anki-integration/duplicate.ts b/src/anki-integration/duplicate.ts index 52ed7ff..c4084ff 100644 --- a/src/anki-integration/duplicate.ts +++ b/src/anki-integration/duplicate.ts @@ -32,9 +32,7 @@ export async function findDuplicateNote( ); const deckValue = deps.getDeck(); - const queryPrefixes = deckValue - ? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, ''] - : ['']; + const queryPrefixes = deckValue ? [`"deck:${escapeAnkiSearchValue(deckValue)}" `, ''] : ['']; try { const noteIds = new Set(); diff --git a/src/anki-integration/field-grouping-workflow.ts b/src/anki-integration/field-grouping-workflow.ts index e9555d0..3990af9 100644 --- a/src/anki-integration/field-grouping-workflow.ts +++ b/src/anki-integration/field-grouping-workflow.ts @@ -112,12 +112,7 @@ export class FieldGroupingWorkflow { const keepNoteId = choice.keepNoteId; const deleteNoteId = choice.deleteNoteId; - await this.performMerge( - keepNoteId, - deleteNoteId, - expression, - choice.deleteDuplicate, - ); + await this.performMerge(keepNoteId, deleteNoteId, expression, choice.deleteDuplicate); return true; } catch (error) { this.deps.logError('Field grouping manual merge failed:', (error as Error).message); diff --git a/src/anki-integration/note-update-workflow.test.ts b/src/anki-integration/note-update-workflow.test.ts index c953ef5..dc69c63 100644 --- a/src/anki-integration/note-update-workflow.test.ts +++ b/src/anki-integration/note-update-workflow.test.ts @@ -51,18 +51,10 @@ function createWorkflowHarness() { return out; }, findDuplicateNote: async (_expression, _excludeNoteId, _noteInfo) => null, - handleFieldGroupingAuto: async ( - _originalNoteId, - _newNoteId, - _newNoteInfo, - _expression, - ) => undefined, - handleFieldGroupingManual: async ( - _originalNoteId, - _newNoteId, - _newNoteInfo, - _expression, - ) => false, + handleFieldGroupingAuto: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) => + undefined, + handleFieldGroupingManual: async (_originalNoteId, _newNoteId, _newNoteInfo, _expression) => + false, processSentence: (text: string, _noteFields: Record) => text, resolveConfiguredFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo, preferred?: string) => { if (!preferred) return null; diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 5753c14..0ce7b5d 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -748,6 +748,9 @@ test('runtime options registry is centralized', () => { const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id); assert.deepEqual(ids, [ 'anki.autoUpdateNewCards', + 'subtitle.annotation.nPlusOne', + 'subtitle.annotation.jlpt', + 'subtitle.annotation.frequency', 'anki.nPlusOneMatchMode', 'anki.kikuFieldGrouping', ]); diff --git a/src/config/definitions/domain-registry.test.ts b/src/config/definitions/domain-registry.test.ts index bf084e4..864f063 100644 --- a/src/config/definitions/domain-registry.test.ts +++ b/src/config/definitions/domain-registry.test.ts @@ -62,7 +62,9 @@ test('domain registry builders each contribute entries to composed registry', () }); test('default keybindings include primary and secondary subtitle track cycling on J keys', () => { - const keybindingMap = new Map(DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command])); + const keybindingMap = new Map( + DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]), + ); assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']); assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']); }); diff --git a/src/config/definitions/options-integrations.ts b/src/config/definitions/options-integrations.ts index 7e48ce9..f85fb61 100644 --- a/src/config/definitions/options-integrations.ts +++ b/src/config/definitions/options-integrations.ts @@ -5,6 +5,8 @@ export function buildIntegrationConfigOptionRegistry( defaultConfig: ResolvedConfig, runtimeOptionRegistry: RuntimeOptionRegistryEntry[], ): ConfigOptionRegistryEntry[] { + const runtimeOptionById = new Map(runtimeOptionRegistry.map((entry) => [entry.id, entry])); + return [ { path: 'ankiConnect.enabled', @@ -54,7 +56,7 @@ export function buildIntegrationConfigOptionRegistry( kind: 'boolean', defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards, description: 'Automatically update newly added cards.', - runtime: runtimeOptionRegistry[0], + runtime: runtimeOptionById.get('anki.autoUpdateNewCards'), }, { path: 'ankiConnect.nPlusOne.matchMode', @@ -105,7 +107,7 @@ export function buildIntegrationConfigOptionRegistry( enumValues: ['auto', 'manual', 'disabled'], defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping, description: 'Kiku duplicate-card field grouping mode.', - runtime: runtimeOptionRegistry[1], + runtime: runtimeOptionById.get('anki.kikuFieldGrouping'), }, { path: 'jimaku.languagePreference', diff --git a/src/config/definitions/runtime-options.ts b/src/config/definitions/runtime-options.ts index e35dade..58a4b3a 100644 --- a/src/config/definitions/runtime-options.ts +++ b/src/config/definitions/runtime-options.ts @@ -19,6 +19,42 @@ export function buildRuntimeOptionRegistry( behavior: { autoUpdateNewCards: value === true }, }), }, + { + id: 'subtitle.annotation.nPlusOne', + path: 'ankiConnect.nPlusOne.highlightEnabled', + label: 'N+1 Annotation', + scope: 'subtitle', + valueType: 'boolean', + allowedValues: [true, false], + defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled, + requiresRestart: false, + formatValueForOsd: (value) => (value === true ? 'On' : 'Off'), + toAnkiPatch: () => ({}), + }, + { + id: 'subtitle.annotation.jlpt', + path: 'subtitleStyle.enableJlpt', + label: 'JLPT Annotation', + scope: 'subtitle', + valueType: 'boolean', + allowedValues: [true, false], + defaultValue: defaultConfig.subtitleStyle.enableJlpt, + requiresRestart: false, + formatValueForOsd: (value) => (value === true ? 'On' : 'Off'), + toAnkiPatch: () => ({}), + }, + { + id: 'subtitle.annotation.frequency', + path: 'subtitleStyle.frequencyDictionary.enabled', + label: 'Frequency Annotation', + scope: 'subtitle', + valueType: 'boolean', + allowedValues: [true, false], + defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.enabled, + requiresRestart: false, + formatValueForOsd: (value) => (value === true ? 'On' : 'Off'), + toAnkiPatch: () => ({}), + }, { id: 'anki.nPlusOneMatchMode', path: 'ankiConnect.nPlusOne.matchMode', diff --git a/src/config/resolve/subtitle-domains.ts b/src/config/resolve/subtitle-domains.ts index 39e7349..cb3020d 100644 --- a/src/config/resolve/subtitle-domains.ts +++ b/src/config/resolve/subtitle-domains.ts @@ -111,7 +111,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { frequencyDictionary: { ...resolved.subtitleStyle.frequencyDictionary, ...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary) - ? ((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary']) + ? ((src.subtitleStyle as { frequencyDictionary?: unknown }) + .frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary']) : {}), }, secondary: { @@ -152,7 +153,9 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { ); } - const hoverTokenColor = asColor((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor); + const hoverTokenColor = asColor( + (src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor, + ); if (hoverTokenColor !== undefined) { resolved.subtitleStyle.hoverTokenColor = hoverTokenColor; } else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) { @@ -174,7 +177,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !== undefined ) { - resolved.subtitleStyle.hoverTokenBackgroundColor = fallbackSubtitleStyleHoverTokenBackgroundColor; + resolved.subtitleStyle.hoverTokenBackgroundColor = + fallbackSubtitleStyleHoverTokenBackgroundColor; warn( 'subtitleStyle.hoverTokenBackgroundColor', (src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor, @@ -208,7 +212,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { if (sourcePath !== undefined) { resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath; } else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) { - resolved.subtitleStyle.frequencyDictionary.sourcePath = fallbackFrequencyDictionary.sourcePath; + resolved.subtitleStyle.frequencyDictionary.sourcePath = + fallbackFrequencyDictionary.sourcePath; warn( 'subtitleStyle.frequencyDictionary.sourcePath', (frequencyDictionary as { sourcePath?: unknown }).sourcePath, @@ -260,7 +265,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { if (singleColor !== undefined) { resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor; } else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) { - resolved.subtitleStyle.frequencyDictionary.singleColor = fallbackFrequencyDictionary.singleColor; + resolved.subtitleStyle.frequencyDictionary.singleColor = + fallbackFrequencyDictionary.singleColor; warn( 'subtitleStyle.frequencyDictionary.singleColor', (frequencyDictionary as { singleColor?: unknown }).singleColor, diff --git a/src/config/template.ts b/src/config/template.ts index 325e90d..49bf9da 100644 --- a/src/config/template.ts +++ b/src/config/template.ts @@ -25,7 +25,8 @@ function humanizeKey(key: string): string { function buildInlineOptionComment(path: string, value: unknown): string { const registryEntry = OPTION_REGISTRY_BY_PATH.get(path); - const baseDescription = registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path); + const baseDescription = + registryEntry?.description ?? TOP_LEVEL_SECTION_DESCRIPTION_BY_KEY.get(path); const description = baseDescription && baseDescription.trim().length > 0 ? normalizeCommentText(baseDescription) diff --git a/src/core/services/anilist/anilist-token-store.ts b/src/core/services/anilist/anilist-token-store.ts index a3c6ca1..a1739da 100644 --- a/src/core/services/anilist/anilist-token-store.ts +++ b/src/core/services/anilist/anilist-token-store.ts @@ -132,16 +132,15 @@ export function createAnilistTokenStore( } return decrypted; } - if ( - typeof parsed.plaintextToken === 'string' && - parsed.plaintextToken.trim().length > 0 - ) { + if (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) { if (storage.isEncryptionAvailable()) { if (!isSafeStorageUsable()) { return null; } const plaintext = parsed.plaintextToken.trim(); - notifyUser('AniList token plaintext fallback payload found. Migrating to encrypted storage.'); + notifyUser( + 'AniList token plaintext fallback payload found. Migrating to encrypted storage.', + ); this.saveToken(plaintext); return plaintext; } diff --git a/src/core/services/cli-command.ts b/src/core/services/cli-command.ts index 0d2ec35..61740ed 100644 --- a/src/core/services/cli-command.ts +++ b/src/core/services/cli-command.ts @@ -283,8 +283,7 @@ export function handleCliCommand( return; } - const shouldStart = - args.start || args.toggle || args.toggleVisibleOverlay; + const shouldStart = args.start || args.toggle || args.toggleVisibleOverlay; const needsOverlayRuntime = commandNeedsOverlayRuntime(args); const shouldInitializeOverlayRuntime = needsOverlayRuntime || args.start; diff --git a/src/core/services/frequency-dictionary.test.ts b/src/core/services/frequency-dictionary.test.ts index 502e061..a785f78 100644 --- a/src/core/services/frequency-dictionary.test.ts +++ b/src/core/services/frequency-dictionary.test.ts @@ -71,7 +71,8 @@ test('createFrequencyDictionaryLookup aggregates duplicate-term logs into a sing assert.equal(lookup('猫'), 100); assert.equal( - logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries')).length, + logs.filter((entry) => entry.includes('Frequency dictionary ignored 2 duplicate term entries')) + .length, 1, ); assert.equal( diff --git a/src/core/services/frequency-dictionary.ts b/src/core/services/frequency-dictionary.ts index 5f9a9a2..9b4d0fb 100644 --- a/src/core/services/frequency-dictionary.ts +++ b/src/core/services/frequency-dictionary.ts @@ -32,7 +32,7 @@ function parsePositiveFrequencyString(value: string): number | null { const chunks = numericPrefix.split(','); const normalizedNumber = chunks.length <= 1 - ? chunks[0] ?? '' + ? (chunks[0] ?? '') : chunks.slice(1).every((chunk) => /^\d{3}$/.test(chunk)) ? chunks.join('') : (chunks[0] ?? ''); diff --git a/src/core/services/index.ts b/src/core/services/index.ts index 6050406..622e56a 100644 --- a/src/core/services/index.ts +++ b/src/core/services/index.ts @@ -62,10 +62,7 @@ export { updateOverlayWindowBounds, } from './overlay-window'; export { initializeOverlayRuntime } from './overlay-runtime-init'; -export { - setVisibleOverlayVisible, - updateVisibleOverlayVisibility, -} from './overlay-visibility'; +export { setVisibleOverlayVisible, updateVisibleOverlayVisibility } from './overlay-visibility'; export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY, MpvIpcClient, diff --git a/src/core/services/ipc.test.ts b/src/core/services/ipc.test.ts index 3e70ebb..af75aaf 100644 --- a/src/core/services/ipc.test.ts +++ b/src/core/services/ipc.test.ts @@ -150,7 +150,12 @@ test('registerIpcHandlers rejects malformed runtime-option payloads', async () = }); const validResult = await setHandler!({}, 'anki.autoUpdateNewCards', true); assert.deepEqual(validResult, { ok: true }); - assert.deepEqual(calls, [{ id: 'anki.autoUpdateNewCards', value: true }]); + const validSubtitleAnnotationResult = await setHandler!({}, 'subtitle.annotation.jlpt', false); + assert.deepEqual(validSubtitleAnnotationResult, { ok: true }); + assert.deepEqual(calls, [ + { id: 'anki.autoUpdateNewCards', value: true }, + { id: 'subtitle.annotation.jlpt', value: false }, + ]); const cycleHandler = handlers.handle.get(IPC_CHANNELS.request.cycleRuntimeOption); assert.ok(cycleHandler); @@ -215,9 +220,7 @@ test('registerIpcHandlers ignores malformed fire-and-forget payloads', () => { handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 'bad' }); handlers.on.get(IPC_CHANNELS.command.saveSubtitlePosition)!({}, { yPercent: 42 }); - assert.deepEqual(saves, [ - { yPercent: 42 }, - ]); + assert.deepEqual(saves, [{ yPercent: 42 }]); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'not-a-modal'); handlers.on.get(IPC_CHANNELS.command.overlayModalClosed)!({}, 'subsync'); diff --git a/src/core/services/jellyfin-token-store.ts b/src/core/services/jellyfin-token-store.ts index ce9e38f..f7033a7 100644 --- a/src/core/services/jellyfin-token-store.ts +++ b/src/core/services/jellyfin-token-store.ts @@ -62,7 +62,8 @@ export function createJellyfinTokenStore( } const decrypted = safeStorage.decryptString(encrypted).trim(); const session = JSON.parse(decrypted) as Partial; - const accessToken = typeof session.accessToken === 'string' ? session.accessToken.trim() : ''; + const accessToken = + typeof session.accessToken === 'string' ? session.accessToken.trim() : ''; const userId = typeof session.userId === 'string' ? session.userId.trim() : ''; if (!accessToken || !userId) return null; return { accessToken, userId }; @@ -88,7 +89,9 @@ export function createJellyfinTokenStore( (typeof parsed.encryptedToken === 'string' && parsed.encryptedToken.length > 0) || (typeof parsed.plaintextToken === 'string' && parsed.plaintextToken.trim().length > 0) ) { - logger.warn('Ignoring legacy Jellyfin token-only store payload because userId is missing.'); + logger.warn( + 'Ignoring legacy Jellyfin token-only store payload because userId is missing.', + ); } } catch (error) { logger.error('Failed to read Jellyfin session store.', error); diff --git a/src/core/services/overlay-bridge.ts b/src/core/services/overlay-bridge.ts index d4a0f64..616c96c 100644 --- a/src/core/services/overlay-bridge.ts +++ b/src/core/services/overlay-bridge.ts @@ -34,10 +34,7 @@ export function sendToVisibleOverlayRuntime(options: { const isLoading = typeof webContents.isLoading === 'function' ? webContents.isLoading() : false; const currentURL = typeof webContents.getURL === 'function' ? webContents.getURL() : ''; - const isReady = - !isLoading && - currentURL !== '' && - currentURL !== 'about:blank'; + const isReady = !isLoading && currentURL !== '' && currentURL !== 'about:blank'; if (!isReady) { if (typeof webContents.once !== 'function') { diff --git a/src/core/services/overlay-drop.ts b/src/core/services/overlay-drop.ts index 8698307..b899748 100644 --- a/src/core/services/overlay-drop.ts +++ b/src/core/services/overlay-drop.ts @@ -85,7 +85,9 @@ export function parseClipboardVideoPath(text: string): string | null { return isSupportedVideoPath(unquoted) ? unquoted : null; } -export function collectDroppedVideoPaths(dataTransfer: DropDataTransferLike | null | undefined): string[] { +export function collectDroppedVideoPaths( + dataTransfer: DropDataTransferLike | null | undefined, +): string[] { if (!dataTransfer) return []; const out: string[] = []; diff --git a/src/core/services/overlay-manager.test.ts b/src/core/services/overlay-manager.test.ts index 39d3e4f..6f1ca42 100644 --- a/src/core/services/overlay-manager.test.ts +++ b/src/core/services/overlay-manager.test.ts @@ -117,13 +117,9 @@ test('runtime-option broadcast still uses expected channel', () => { }, ); let state = false; - const changed = setOverlayDebugVisualizationEnabledRuntime( - state, - true, - (enabled) => { - state = enabled; - }, - ); + const changed = setOverlayDebugVisualizationEnabledRuntime(state, true, (enabled) => { + state = enabled; + }); assert.equal(changed, true); assert.equal(state, true); assert.deepEqual(broadcasts, [['runtime-options:changed', []]]); diff --git a/src/core/services/overlay-runtime-init.test.ts b/src/core/services/overlay-runtime-init.test.ts index 489f7ba..ec487de 100644 --- a/src/core/services/overlay-runtime-init.test.ts +++ b/src/core/services/overlay-runtime-init.test.ts @@ -41,13 +41,12 @@ test('initializeOverlayRuntime skips Anki integration when ankiConnect.enabled i setIntegrationCalls += 1; }, showDesktopNotification: () => {}, - createFieldGroupingCallback: () => - async () => ({ - keepNoteId: 1, - deleteNoteId: 2, - deleteDuplicate: false, - cancelled: false, - }), + createFieldGroupingCallback: () => async () => ({ + keepNoteId: 1, + deleteNoteId: 2, + deleteDuplicate: false, + cancelled: false, + }), getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json', }); @@ -96,13 +95,12 @@ test('initializeOverlayRuntime starts Anki integration when ankiConnect.enabled setIntegrationCalls += 1; }, showDesktopNotification: () => {}, - createFieldGroupingCallback: () => - async () => ({ - keepNoteId: 3, - deleteNoteId: 4, - deleteDuplicate: false, - cancelled: false, - }), + createFieldGroupingCallback: () => async () => ({ + keepNoteId: 3, + deleteNoteId: 4, + deleteDuplicate: false, + cancelled: false, + }), getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json', }); diff --git a/src/core/services/overlay-runtime-init.ts b/src/core/services/overlay-runtime-init.ts index 117b8b3..daca203 100644 --- a/src/core/services/overlay-runtime-init.ts +++ b/src/core/services/overlay-runtime-init.ts @@ -23,7 +23,8 @@ type CreateAnkiIntegrationArgs = { }; function createDefaultAnkiIntegration(args: CreateAnkiIntegrationArgs): AnkiIntegrationLike { - const { AnkiIntegration } = require('../../anki-integration') as typeof import('../../anki-integration'); + const { AnkiIntegration } = + require('../../anki-integration') as typeof import('../../anki-integration'); return new AnkiIntegration( args.config, args.subtitleTimingTracker as never, @@ -76,7 +77,10 @@ export function initializeOverlayRuntime(options: { options.registerGlobalShortcuts(); const createWindowTrackerHandler = options.createWindowTracker ?? createWindowTracker; - const windowTracker = createWindowTrackerHandler(options.backendOverride, options.getMpvSocketPath()); + const windowTracker = createWindowTrackerHandler( + options.backendOverride, + options.getMpvSocketPath(), + ); options.setWindowTracker(windowTracker); if (windowTracker) { windowTracker.onGeometryChange = (geometry: WindowGeometry) => { diff --git a/src/core/services/subtitle-processing-controller.test.ts b/src/core/services/subtitle-processing-controller.test.ts index 5c44483..860eb5e 100644 --- a/src/core/services/subtitle-processing-controller.test.ts +++ b/src/core/services/subtitle-processing-controller.test.ts @@ -138,3 +138,35 @@ test('subtitle processing refresh can use explicit text override', async () => { assert.deepEqual(emitted, [{ text: 'initial', tokens: [] }]); }); + +test('subtitle processing cache invalidation only affects future subtitle events', async () => { + const emitted: SubtitleData[] = []; + const callsByText = new Map(); + const controller = createSubtitleProcessingController({ + tokenizeSubtitle: async (text) => { + callsByText.set(text, (callsByText.get(text) ?? 0) + 1); + return { text, tokens: [] }; + }, + emitSubtitle: (payload) => emitted.push(payload), + }); + + controller.onSubtitleChange('same'); + await flushMicrotasks(); + controller.onSubtitleChange('other'); + await flushMicrotasks(); + controller.onSubtitleChange('same'); + await flushMicrotasks(); + + assert.equal(callsByText.get('same'), 1); + assert.equal(emitted.length, 3); + + controller.invalidateTokenizationCache(); + assert.equal(emitted.length, 3); + + controller.onSubtitleChange('different'); + await flushMicrotasks(); + controller.onSubtitleChange('same'); + await flushMicrotasks(); + + assert.equal(callsByText.get('same'), 2); +}); diff --git a/src/core/services/subtitle-processing-controller.ts b/src/core/services/subtitle-processing-controller.ts index 69f1d45..20ea805 100644 --- a/src/core/services/subtitle-processing-controller.ts +++ b/src/core/services/subtitle-processing-controller.ts @@ -10,6 +10,7 @@ export interface SubtitleProcessingControllerDeps { export interface SubtitleProcessingController { onSubtitleChange: (text: string) => void; refreshCurrentSubtitle: (textOverride?: string) => void; + invalidateTokenizationCache: () => void; } export function createSubtitleProcessingController( @@ -126,5 +127,8 @@ export function createSubtitleProcessingController( refreshRequested = true; processLatest(); }, + invalidateTokenizationCache: () => { + tokenizationCache.clear(); + }, }; } diff --git a/src/core/services/tokenizer/annotation-stage.test.ts b/src/core/services/tokenizer/annotation-stage.test.ts index 50d2cbd..a09400a 100644 --- a/src/core/services/tokenizer/annotation-stage.test.ts +++ b/src/core/services/tokenizer/annotation-stage.test.ts @@ -52,7 +52,12 @@ test('annotateTokens known-word match mode uses headword vs surface', () => { test('annotateTokens excludes frequency for particle/bound_auxiliary and pos1 exclusions', () => { const tokens = [ - makeToken({ surface: 'は', headword: 'は', partOfSpeech: PartOfSpeech.particle, frequencyRank: 3 }), + makeToken({ + surface: 'は', + headword: 'は', + partOfSpeech: PartOfSpeech.particle, + frequencyRank: 3, + }), makeToken({ surface: 'です', headword: 'です', diff --git a/src/core/services/tokenizer/annotation-stage.ts b/src/core/services/tokenizer/annotation-stage.ts index 52d36c0..ab68274 100644 --- a/src/core/services/tokenizer/annotation-stage.ts +++ b/src/core/services/tokenizer/annotation-stage.ts @@ -7,12 +7,7 @@ import { DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG, resolveAnnotationPos2ExclusionSet, } from '../../../token-pos2-exclusions'; -import { - JlptLevel, - MergedToken, - NPlusOneMatchMode, - PartOfSpeech, -} from '../../../types'; +import { JlptLevel, MergedToken, NPlusOneMatchMode, PartOfSpeech } from '../../../types'; import { shouldIgnoreJlptByTerm, shouldIgnoreJlptForMecabPos1 } from '../jlpt-token-filter'; const KATAKANA_TO_HIRAGANA_OFFSET = 0x60; @@ -67,10 +62,7 @@ function normalizePos1Tag(pos1: string | undefined): string { return typeof pos1 === 'string' ? pos1.trim() : ''; } -function isExcludedByTagSet( - normalizedTag: string, - exclusions: ReadonlySet, -): boolean { +function isExcludedByTagSet(normalizedTag: string, exclusions: ReadonlySet): boolean { if (!normalizedTag) { return false; } @@ -350,10 +342,7 @@ function isLikelyFrequencyNoiseToken(token: MergedToken): boolean { continue; } - if ( - shouldIgnoreJlptByTerm(trimmedCandidate) || - shouldIgnoreJlptByTerm(normalizedCandidate) - ) { + if (shouldIgnoreJlptByTerm(trimmedCandidate) || shouldIgnoreJlptByTerm(normalizedCandidate)) { return true; } @@ -442,13 +431,12 @@ export function annotateTokens( })); const frequencyEnabled = options.frequencyEnabled !== false; - const frequencyMarkedTokens = - frequencyEnabled - ? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions) - : knownMarkedTokens.map((token) => ({ - ...token, - frequencyRank: undefined, - })); + const frequencyMarkedTokens = frequencyEnabled + ? applyFrequencyMarking(knownMarkedTokens, pos1Exclusions, pos2Exclusions) + : knownMarkedTokens.map((token) => ({ + ...token, + frequencyRank: undefined, + })); const jlptEnabled = options.jlptEnabled !== false; const jlptMarkedTokens = jlptEnabled diff --git a/src/core/services/tokenizer/parser-enrichment-worker-runtime.ts b/src/core/services/tokenizer/parser-enrichment-worker-runtime.ts index 1450d19..1ca592a 100644 --- a/src/core/services/tokenizer/parser-enrichment-worker-runtime.ts +++ b/src/core/services/tokenizer/parser-enrichment-worker-runtime.ts @@ -116,7 +116,9 @@ class ParserEnrichmentWorkerRuntime { } private handleWorkerFailure(error: Error): void { - logger.debug(`Parser enrichment worker unavailable, falling back to main thread: ${error.message}`); + logger.debug( + `Parser enrichment worker unavailable, falling back to main thread: ${error.message}`, + ); for (const pending of this.pending.values()) { pending.reject(error); } diff --git a/src/main.ts b/src/main.ts index 4ed428e..af9ea30 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1256,10 +1256,7 @@ function getResolvedConfig() { } function getRuntimeBooleanOption( - id: - | 'subtitle.annotation.nPlusOne' - | 'subtitle.annotation.jlpt' - | 'subtitle.annotation.frequency', + id: 'subtitle.annotation.nPlusOne' | 'subtitle.annotation.jlpt' | 'subtitle.annotation.frequency', fallback: boolean, ): boolean { const value = appState.runtimeOptionsManager?.getOptionValue(id); @@ -2318,7 +2315,10 @@ const { getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords, getJlptLevel: (text) => appState.jlptLevelLookup(text), getJlptEnabled: () => - getRuntimeBooleanOption('subtitle.annotation.jlpt', getResolvedConfig().subtitleStyle.enableJlpt), + getRuntimeBooleanOption( + 'subtitle.annotation.jlpt', + getResolvedConfig().subtitleStyle.enableJlpt, + ), getFrequencyDictionaryEnabled: () => getRuntimeBooleanOption( 'subtitle.annotation.frequency', diff --git a/src/main/overlay-runtime.test.ts b/src/main/overlay-runtime.test.ts index 04b1768..04bbd05 100644 --- a/src/main/overlay-runtime.test.ts +++ b/src/main/overlay-runtime.test.ts @@ -214,9 +214,13 @@ test('handleOverlayModalClosed hides modal window only after all pending modals runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { restoreOnModalClose: 'runtime-options', }); - runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, { - restoreOnModalClose: 'subsync', - }); + runtime.sendToActiveOverlayWindow( + 'subsync:open-manual', + { sourceTracks: [] }, + { + restoreOnModalClose: 'subsync', + }, + ); runtime.handleOverlayModalClosed('runtime-options'); assert.equal(window.getHideCount(), 0); @@ -267,9 +271,13 @@ test('modal runtime notifies callers when modal input state becomes active/inact runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, { restoreOnModalClose: 'runtime-options', }); - runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, { - restoreOnModalClose: 'subsync', - }); + runtime.sendToActiveOverlayWindow( + 'subsync:open-manual', + { sourceTracks: [] }, + { + restoreOnModalClose: 'subsync', + }, + ); assert.deepEqual(state, []); runtime.notifyOverlayModalOpened('runtime-options'); assert.deepEqual(state, [true]); @@ -352,9 +360,13 @@ test('handleOverlayModalClosed hides modal window for single kiku modal', () => setModalWindowBounds: () => {}, }); - runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, { - restoreOnModalClose: 'kiku', - }); + runtime.sendToActiveOverlayWindow( + 'kiku:field-grouping-open', + { test: true }, + { + restoreOnModalClose: 'kiku', + }, + ); runtime.handleOverlayModalClosed('kiku'); assert.equal(window.getHideCount(), 1); diff --git a/src/main/runtime/anilist-media-guess-main-deps.ts b/src/main/runtime/anilist-media-guess-main-deps.ts index 2a5bfeb..58274a2 100644 --- a/src/main/runtime/anilist-media-guess-main-deps.ts +++ b/src/main/runtime/anilist-media-guess-main-deps.ts @@ -3,7 +3,9 @@ import type { createMaybeProbeAnilistDurationHandler, } from './anilist-media-guess'; -type MaybeProbeAnilistDurationMainDeps = Parameters[0]; +type MaybeProbeAnilistDurationMainDeps = Parameters< + typeof createMaybeProbeAnilistDurationHandler +>[0]; type EnsureAnilistMediaGuessMainDeps = Parameters[0]; export function createBuildMaybeProbeAnilistDurationMainDepsHandler( @@ -19,13 +21,17 @@ export function createBuildMaybeProbeAnilistDurationMainDepsHandler( }); } -export function createBuildEnsureAnilistMediaGuessMainDepsHandler(deps: EnsureAnilistMediaGuessMainDeps) { +export function createBuildEnsureAnilistMediaGuessMainDepsHandler( + deps: EnsureAnilistMediaGuessMainDeps, +) { return (): EnsureAnilistMediaGuessMainDeps => ({ getState: () => deps.getState(), setState: (state) => deps.setState(state), - resolveMediaPathForJimaku: (currentMediaPath) => deps.resolveMediaPathForJimaku(currentMediaPath), + resolveMediaPathForJimaku: (currentMediaPath) => + deps.resolveMediaPathForJimaku(currentMediaPath), getCurrentMediaPath: () => deps.getCurrentMediaPath(), getCurrentMediaTitle: () => deps.getCurrentMediaTitle(), - guessAnilistMediaInfo: (mediaPath, mediaTitle) => deps.guessAnilistMediaInfo(mediaPath, mediaTitle), + guessAnilistMediaInfo: (mediaPath, mediaTitle) => + deps.guessAnilistMediaInfo(mediaPath, mediaTitle), }); } diff --git a/src/main/runtime/anilist-media-state-main-deps.ts b/src/main/runtime/anilist-media-state-main-deps.ts index 7057cd2..c0139b5 100644 --- a/src/main/runtime/anilist-media-state-main-deps.ts +++ b/src/main/runtime/anilist-media-state-main-deps.ts @@ -6,8 +6,12 @@ import type { createSetAnilistMediaGuessRuntimeStateHandler, } from './anilist-media-state'; -type GetCurrentAnilistMediaKeyMainDeps = Parameters[0]; -type ResetAnilistMediaTrackingMainDeps = Parameters[0]; +type GetCurrentAnilistMediaKeyMainDeps = Parameters< + typeof createGetCurrentAnilistMediaKeyHandler +>[0]; +type ResetAnilistMediaTrackingMainDeps = Parameters< + typeof createResetAnilistMediaTrackingHandler +>[0]; type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters< typeof createGetAnilistMediaGuessRuntimeStateHandler >[0]; diff --git a/src/main/runtime/anilist-post-watch-main-deps.ts b/src/main/runtime/anilist-post-watch-main-deps.ts index b5a1a45..7a011bd 100644 --- a/src/main/runtime/anilist-post-watch-main-deps.ts +++ b/src/main/runtime/anilist-post-watch-main-deps.ts @@ -47,7 +47,8 @@ export function createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler( hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key), processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(), refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(), - enqueueRetry: (key: string, title: string, episode: number) => deps.enqueueRetry(key, title, episode), + enqueueRetry: (key: string, title: string, episode: number) => + deps.enqueueRetry(key, title, episode), markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message), markRetrySuccess: (key: string) => deps.markRetrySuccess(key), refreshRetryQueueState: () => deps.refreshRetryQueueState(), diff --git a/src/main/runtime/anilist-post-watch.ts b/src/main/runtime/anilist-post-watch.ts index 1deb055..27e535b 100644 --- a/src/main/runtime/anilist-post-watch.ts +++ b/src/main/runtime/anilist-post-watch.ts @@ -64,7 +64,11 @@ export function createProcessNextAnilistRetryUpdateHandler(deps: { return { ok: false, message: 'AniList token unavailable for queued retry.' }; } - const result = await deps.updateAnilistPostWatchProgress(accessToken, queued.title, queued.episode); + const result = await deps.updateAnilistPostWatchProgress( + accessToken, + queued.title, + queued.episode, + ); if (result.status === 'updated' || result.status === 'skipped') { deps.markSuccess(queued.key); deps.rememberAttemptedUpdateKey(queued.key); @@ -166,7 +170,11 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: { return; } - const result = await deps.updateAnilistPostWatchProgress(accessToken, guess.title, guess.episode); + const result = await deps.updateAnilistPostWatchProgress( + accessToken, + guess.title, + guess.episode, + ); if (result.status === 'updated') { deps.rememberAttemptedUpdateKey(attemptKey); deps.markRetrySuccess(attemptKey); diff --git a/src/main/runtime/anilist-setup-protocol-main-deps.ts b/src/main/runtime/anilist-setup-protocol-main-deps.ts index b028a2a..6532908 100644 --- a/src/main/runtime/anilist-setup-protocol-main-deps.ts +++ b/src/main/runtime/anilist-setup-protocol-main-deps.ts @@ -44,7 +44,8 @@ export function createBuildHandleAnilistSetupProtocolUrlMainDepsHandler( deps: HandleAnilistSetupProtocolUrlMainDeps, ) { return (): HandleAnilistSetupProtocolUrlMainDeps => ({ - consumeAnilistSetupTokenFromUrl: (rawUrl: string) => deps.consumeAnilistSetupTokenFromUrl(rawUrl), + consumeAnilistSetupTokenFromUrl: (rawUrl: string) => + deps.consumeAnilistSetupTokenFromUrl(rawUrl), logWarn: (message: string, details: unknown) => deps.logWarn(message, details), }); } diff --git a/src/main/runtime/anilist-setup-protocol.ts b/src/main/runtime/anilist-setup-protocol.ts index 6c119ce..951b05f 100644 --- a/src/main/runtime/anilist-setup-protocol.ts +++ b/src/main/runtime/anilist-setup-protocol.ts @@ -66,11 +66,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: { getArgv: () => string[]; execPath: string; resolvePath: (value: string) => string; - setAsDefaultProtocolClient: ( - scheme: string, - path?: string, - args?: string[], - ) => boolean; + setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean; logWarn: (message: string, details?: unknown) => void; }) { return (): void => { diff --git a/src/main/runtime/anilist-setup-window.test.ts b/src/main/runtime/anilist-setup-window.test.ts index 96b37d3..20e91d4 100644 --- a/src/main/runtime/anilist-setup-window.test.ts +++ b/src/main/runtime/anilist-setup-window.test.ts @@ -259,7 +259,8 @@ test('open anilist setup handler no-ops when existing setup window focused', () test('open anilist setup handler wires navigation, fallback, and lifecycle', () => { let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null; - let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null; + let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = + null; let didNavigateHandler: ((event: unknown, url: string) => void) | null = null; let didFinishLoadHandler: (() => void) | null = null; let didFailLoadHandler: @@ -276,7 +277,12 @@ test('open anilist setup handler wires navigation, fallback, and lifecycle', () openHandler = handler; }, on: ( - event: 'will-navigate' | 'will-redirect' | 'did-navigate' | 'did-fail-load' | 'did-finish-load', + event: + | 'will-navigate' + | 'will-redirect' + | 'did-navigate' + | 'did-fail-load' + | 'did-finish-load', handler: (...args: any[]) => void, ) => { if (event === 'will-navigate') willNavigateHandler = handler as never; diff --git a/src/main/runtime/anilist-setup-window.ts b/src/main/runtime/anilist-setup-window.ts index 636f404..6b49ca2 100644 --- a/src/main/runtime/anilist-setup-window.ts +++ b/src/main/runtime/anilist-setup-window.ts @@ -126,7 +126,11 @@ export function createAnilistSetupDidNavigateHandler(deps: { } export function createAnilistSetupDidFailLoadHandler(deps: { - onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => void; + onLoadFailure: (details: { + errorCode: number; + errorDescription: string; + validatedURL: string; + }) => void; }) { return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => { deps.onLoadFailure(details); @@ -175,7 +179,11 @@ export function createAnilistSetupFallbackHandler(deps: { logWarn: (message: string) => void; }) { return { - onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => { + onLoadFailure: (details: { + errorCode: number; + errorDescription: string; + validatedURL: string; + }) => { deps.logError('AniList setup window failed to load', details); deps.openSetupInBrowser(); if (!deps.setupWindow.isDestroyed()) { @@ -298,12 +306,7 @@ export function createOpenAnilistSetupWindowHandler { + (_event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => { handleDidFailLoad({ errorCode, errorDescription, diff --git a/src/main/runtime/anilist-setup.ts b/src/main/runtime/anilist-setup.ts index afca57a..8d55df6 100644 --- a/src/main/runtime/anilist-setup.ts +++ b/src/main/runtime/anilist-setup.ts @@ -65,9 +65,7 @@ export function findAnilistSetupDeepLinkArgvUrl(argv: readonly string[]): string return null; } -export function consumeAnilistSetupCallbackUrl( - deps: ConsumeAnilistSetupCallbackUrlDeps, -): boolean { +export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallbackUrlDeps): boolean { const token = extractAnilistAccessTokenFromUrl(deps.rawUrl); if (!token) { return false; diff --git a/src/main/runtime/anilist-token-refresh.ts b/src/main/runtime/anilist-token-refresh.ts index 583eda1..f1a98e2 100644 --- a/src/main/runtime/anilist-token-refresh.ts +++ b/src/main/runtime/anilist-token-refresh.ts @@ -12,7 +12,9 @@ type ConfigWithAnilistToken = { }; }; -export function createRefreshAnilistClientSecretStateHandler(deps: { +export function createRefreshAnilistClientSecretStateHandler< + TConfig extends ConfigWithAnilistToken, +>(deps: { getResolvedConfig: () => TConfig; isAnilistTrackingEnabled: (config: TConfig) => boolean; getCachedAccessToken: () => string | null; diff --git a/src/main/runtime/anki-actions-main-deps.ts b/src/main/runtime/anki-actions-main-deps.ts index 8317a57..eda1e8c 100644 --- a/src/main/runtime/anki-actions-main-deps.ts +++ b/src/main/runtime/anki-actions-main-deps.ts @@ -24,7 +24,9 @@ export function createBuildUpdateLastCardFromClipboardMainDepsHandler(dep }); } -export function createBuildRefreshKnownWordCacheMainDepsHandler(deps: RefreshKnownWordCacheMainDeps) { +export function createBuildRefreshKnownWordCacheMainDepsHandler( + deps: RefreshKnownWordCacheMainDeps, +) { return (): RefreshKnownWordCacheMainDeps => ({ getAnkiIntegration: () => deps.getAnkiIntegration(), missingIntegrationMessage: deps.missingIntegrationMessage, @@ -42,8 +44,10 @@ export function createBuildTriggerFieldGroupingMainDepsHandler(deps: { return () => ({ getAnkiIntegration: () => deps.getAnkiIntegration(), showMpvOsd: (text: string) => deps.showMpvOsd(text), - triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => - deps.triggerFieldGroupingCore(options), + triggerFieldGroupingCore: (options: { + ankiIntegration: TAnki; + showMpvOsd: (text: string) => void; + }) => deps.triggerFieldGroupingCore(options), }); } @@ -58,8 +62,10 @@ export function createBuildMarkLastCardAsAudioCardMainDepsHandler(deps: { return () => ({ getAnkiIntegration: () => deps.getAnkiIntegration(), showMpvOsd: (text: string) => deps.showMpvOsd(text), - markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => - deps.markLastCardAsAudioCardCore(options), + markLastCardAsAudioCardCore: (options: { + ankiIntegration: TAnki; + showMpvOsd: (text: string) => void; + }) => deps.markLastCardAsAudioCardCore(options), }); } diff --git a/src/main/runtime/app-lifecycle-main-cleanup.ts b/src/main/runtime/app-lifecycle-main-cleanup.ts index 21ba6d8..b8b073c 100644 --- a/src/main/runtime/app-lifecycle-main-cleanup.ts +++ b/src/main/runtime/app-lifecycle-main-cleanup.ts @@ -52,8 +52,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: { destroyTray: () => deps.destroyTray(), stopConfigHotReload: () => deps.stopConfigHotReload(), restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(), - restoreMpvSubVisibility: () => - deps.restoreMpvSubVisibility(), + restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(), unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(), stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(), stopTexthookerService: () => deps.stopTexthookerService(), diff --git a/src/main/runtime/app-ready-main-deps.ts b/src/main/runtime/app-ready-main-deps.ts index 74fcbd0..aff812b 100644 --- a/src/main/runtime/app-ready-main-deps.ts +++ b/src/main/runtime/app-ready-main-deps.ts @@ -1,8 +1,6 @@ import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle'; -export function createBuildAppReadyRuntimeMainDepsHandler( - deps: AppReadyRuntimeDepsFactoryInput, -) { +export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeDepsFactoryInput) { return (): AppReadyRuntimeDepsFactoryInput => ({ loadSubtitlePosition: deps.loadSubtitlePosition, resolveKeybindings: deps.resolveKeybindings, @@ -27,8 +25,7 @@ export function createBuildAppReadyRuntimeMainDepsHandler( prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries, startBackgroundWarmups: deps.startBackgroundWarmups, texthookerOnlyMode: deps.texthookerOnlyMode, - shouldAutoInitializeOverlayRuntimeFromConfig: - deps.shouldAutoInitializeOverlayRuntimeFromConfig, + shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig, initializeOverlayRuntime: deps.initializeOverlayRuntime, handleInitialArgs: deps.handleInitialArgs, onCriticalConfigErrors: deps.onCriticalConfigErrors, diff --git a/src/main/runtime/app-runtime-main-deps.test.ts b/src/main/runtime/app-runtime-main-deps.test.ts index 37252d5..23e6cc1 100644 --- a/src/main/runtime/app-runtime-main-deps.test.ts +++ b/src/main/runtime/app-runtime-main-deps.test.ts @@ -70,7 +70,8 @@ test('open yomitan settings main deps map async open callbacks', async () => { const extension = { id: 'ext' }; const deps = createBuildOpenYomitanSettingsMainDepsHandler({ ensureYomitanExtensionLoaded: async () => extension, - openYomitanSettingsWindow: ({ yomitanExt }) => calls.push(`open:${(yomitanExt as { id: string }).id}`), + openYomitanSettingsWindow: ({ yomitanExt }) => + calls.push(`open:${(yomitanExt as { id: string }).id}`), getExistingWindow: () => currentWindow, setWindow: (window) => { currentWindow = window; diff --git a/src/main/runtime/cli-command-context-factory.ts b/src/main/runtime/cli-command-context-factory.ts index 0542236..de62ff6 100644 --- a/src/main/runtime/cli-command-context-factory.ts +++ b/src/main/runtime/cli-command-context-factory.ts @@ -2,9 +2,7 @@ import { createCliCommandContext } from './cli-command-context'; import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps'; import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps'; -type CliCommandContextMainDeps = Parameters< - typeof createBuildCliCommandContextMainDepsHandler ->[0]; +type CliCommandContextMainDeps = Parameters[0]; export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) { const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps); diff --git a/src/main/runtime/cli-command-context.test.ts b/src/main/runtime/cli-command-context.test.ts index 1417433..1137c8a 100644 --- a/src/main/runtime/cli-command-context.test.ts +++ b/src/main/runtime/cli-command-context.test.ts @@ -34,11 +34,11 @@ function createDeps() { triggerFieldGrouping: async () => {}, triggerSubsyncFromConfig: async () => {}, markLastCardAsAudioCard: async () => {}, - getAnilistStatus: () => ({} as never), + getAnilistStatus: () => ({}) as never, clearAnilistToken: () => {}, openAnilistSetup: () => {}, openJellyfinSetup: () => {}, - getAnilistQueueStatus: () => ({} as never), + getAnilistQueueStatus: () => ({}) as never, retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }), runJellyfinCommand: async () => {}, openYomitanSettings: () => {}, diff --git a/src/main/runtime/cli-command-runtime-handler.ts b/src/main/runtime/cli-command-runtime-handler.ts index 1faeb15..e1eb199 100644 --- a/src/main/runtime/cli-command-runtime-handler.ts +++ b/src/main/runtime/cli-command-runtime-handler.ts @@ -15,12 +15,11 @@ export function createCliCommandRuntimeHandler(deps: { cliContext: TCliContext, ) => void; }) { - const handleTexthookerOnlyModeTransitionHandler = - createHandleTexthookerOnlyModeTransitionHandler( - createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler( - deps.handleTexthookerOnlyModeTransitionMainDeps, - )(), - ); + const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler( + createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler( + deps.handleTexthookerOnlyModeTransitionMainDeps, + )(), + ); return (args: CliArgs, source: CliCommandSource = 'initial'): void => { handleTexthookerOnlyModeTransitionHandler(args); diff --git a/src/main/runtime/clipboard-queue.ts b/src/main/runtime/clipboard-queue.ts index b718af8..aa209ed 100644 --- a/src/main/runtime/clipboard-queue.ts +++ b/src/main/runtime/clipboard-queue.ts @@ -13,9 +13,10 @@ export type AppendClipboardVideoToQueueRuntimeDeps = { sendMpvCommand: (command: (string | number)[]) => void; }; -export function appendClipboardVideoToQueueRuntime( - deps: AppendClipboardVideoToQueueRuntimeDeps, -): { ok: boolean; message: string } { +export function appendClipboardVideoToQueueRuntime(deps: AppendClipboardVideoToQueueRuntimeDeps): { + ok: boolean; + message: string; +} { const mpvClient = deps.getMpvClient(); if (!mpvClient || !mpvClient.connected) { return { ok: false, message: 'MPV is not connected.' }; diff --git a/src/main/runtime/composers/jellyfin-remote-composer.test.ts b/src/main/runtime/composers/jellyfin-remote-composer.test.ts index 32782a9..e3fa0f8 100644 --- a/src/main/runtime/composers/jellyfin-remote-composer.test.ts +++ b/src/main/runtime/composers/jellyfin-remote-composer.test.ts @@ -6,7 +6,8 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', let lastProgressAt = 0; const composed = composeJellyfinRemoteHandlers({ getConfiguredSession: () => null, - getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never, + getClientInfo: () => + ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never, getJellyfinConfig: () => ({ enabled: false }) as never, playJellyfinItem: async () => {}, logWarn: () => {}, diff --git a/src/main/runtime/composers/mpv-runtime-composer.ts b/src/main/runtime/composers/mpv-runtime-composer.ts index 898c2d8..b1146af 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.ts @@ -141,7 +141,7 @@ export function composeMpvRuntimeHandlers< if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) { await createMecabTokenizerAndCheck().catch(() => {}); } - await prewarmSubtitleDictionaries({ showLoadingOsd: true }); + await prewarmSubtitleDictionaries({ showLoadingOsd: true }); })().finally(() => { tokenizationWarmupInFlight = null; }); diff --git a/src/main/runtime/config-derived.ts b/src/main/runtime/config-derived.ts index 638e72f..6f7003d 100644 --- a/src/main/runtime/config-derived.ts +++ b/src/main/runtime/config-derived.ts @@ -39,7 +39,10 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): { deps.defaultJimakuLanguagePreference, ), getJimakuMaxEntryResults: () => - getJimakuMaxEntryResultsCore(() => deps.getResolvedConfig(), deps.defaultJimakuMaxEntryResults), + getJimakuMaxEntryResultsCore( + () => deps.getResolvedConfig(), + deps.defaultJimakuMaxEntryResults, + ), resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()), jimakuFetchJson: ( endpoint: string, diff --git a/src/main/runtime/config-hot-reload-main-deps.test.ts b/src/main/runtime/config-hot-reload-main-deps.test.ts index 401e450..137272a 100644 --- a/src/main/runtime/config-hot-reload-main-deps.test.ts +++ b/src/main/runtime/config-hot-reload-main-deps.test.ts @@ -111,7 +111,7 @@ test('config hot reload applied main deps builder maps callbacks', () => { test('config hot reload runtime main deps builder maps runtime callbacks', () => { const calls: string[] = []; const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({ - getCurrentConfig: () => ({ id: 1 } as never as ResolvedConfig), + getCurrentConfig: () => ({ id: 1 }) as never as ResolvedConfig, reloadConfigStrict: () => ({ ok: true, @@ -144,5 +144,10 @@ test('config hot reload runtime main deps builder maps runtime callbacks', () => deps.onRestartRequired([]); deps.onInvalidConfig('bad'); deps.onValidationWarnings('/tmp/config.jsonc', []); - assert.deepEqual(calls, ['hot-reload', 'restart-required', 'invalid-config', 'validation-warnings']); + assert.deepEqual(calls, [ + 'hot-reload', + 'restart-required', + 'invalid-config', + 'validation-warnings', + ]); }); diff --git a/src/main/runtime/config-hot-reload-main-deps.ts b/src/main/runtime/config-hot-reload-main-deps.ts index c563290..ccbef94 100644 --- a/src/main/runtime/config-hot-reload-main-deps.ts +++ b/src/main/runtime/config-hot-reload-main-deps.ts @@ -3,7 +3,12 @@ import type { ConfigHotReloadRuntimeDeps, } from '../../core/services/config-hot-reload'; import type { ReloadConfigStrictResult } from '../../config'; -import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types'; +import type { + ConfigHotReloadPayload, + ConfigValidationWarning, + ResolvedConfig, + SecondarySubMode, +} from '../../types'; import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers'; type ConfigWatchListener = (eventType: string, filename: string | null) => void; diff --git a/src/main/runtime/dictionary-runtime-main-deps.test.ts b/src/main/runtime/dictionary-runtime-main-deps.test.ts index 8fdd4ea..330dea3 100644 --- a/src/main/runtime/dictionary-runtime-main-deps.test.ts +++ b/src/main/runtime/dictionary-runtime-main-deps.test.ts @@ -29,7 +29,8 @@ test('jlpt dictionary runtime main deps builder maps search paths and log prefix const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({ isJlptEnabled: () => true, getDictionaryRoots: () => ['/root/a'], - getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => getDictionaryRoots().map((path) => `${path}/jlpt`), + getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => + getDictionaryRoots().map((path) => `${path}/jlpt`), setJlptLevelLookup: () => calls.push('set-lookup'), logInfo: (message) => calls.push(`log:${message}`), })(); diff --git a/src/main/runtime/global-shortcuts-main-deps.test.ts b/src/main/runtime/global-shortcuts-main-deps.test.ts index e4f086c..e297573 100644 --- a/src/main/runtime/global-shortcuts-main-deps.test.ts +++ b/src/main/runtime/global-shortcuts-main-deps.test.ts @@ -12,20 +12,24 @@ test('get configured shortcuts main deps map config resolver inputs', () => { const build = createBuildGetConfiguredShortcutsMainDepsHandler({ getResolvedConfig: () => config, defaultConfig: defaults, - resolveConfiguredShortcuts: (nextConfig, nextDefaults) => ({ nextConfig, nextDefaults }) as never, + resolveConfiguredShortcuts: (nextConfig, nextDefaults) => + ({ nextConfig, nextDefaults }) as never, }); const deps = build(); assert.equal(deps.getResolvedConfig(), config); assert.equal(deps.defaultConfig, defaults); - assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), { nextConfig: config, nextDefaults: defaults }); + assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), { + nextConfig: config, + nextDefaults: defaults, + }); }); test('register global shortcuts main deps map callbacks and flags', () => { const calls: string[] = []; const mainWindow = { id: 'main' }; const build = createBuildRegisterGlobalShortcutsMainDepsHandler({ - getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never), + getConfiguredShortcuts: () => ({ copySubtitle: 's' }) as never, registerGlobalShortcutsCore: () => calls.push('register'), toggleVisibleOverlay: () => calls.push('toggle-visible'), openYomitanSettings: () => calls.push('open-yomitan'), diff --git a/src/main/runtime/global-shortcuts-runtime-handlers.ts b/src/main/runtime/global-shortcuts-runtime-handlers.ts index 00d6013..2ad6553 100644 --- a/src/main/runtime/global-shortcuts-runtime-handlers.ts +++ b/src/main/runtime/global-shortcuts-runtime-handlers.ts @@ -32,15 +32,17 @@ export function createGlobalShortcutsRuntimeHandlers(deps: { const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler( deps.getConfiguredShortcutsMainDeps, )(); - const getConfiguredShortcutsHandler = - createGetConfiguredShortcutsHandler(getConfiguredShortcutsMainDeps); + const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler( + getConfiguredShortcutsMainDeps, + ); const getConfiguredShortcuts = () => getConfiguredShortcutsHandler(); const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler( deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts), )(); - const registerGlobalShortcutsHandler = - createRegisterGlobalShortcutsHandler(registerGlobalShortcutsMainDeps); + const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler( + registerGlobalShortcutsMainDeps, + ); const registerGlobalShortcuts = () => registerGlobalShortcutsHandler(); const refreshGlobalAndOverlayShortcutsMainDeps = diff --git a/src/main/runtime/global-shortcuts.ts b/src/main/runtime/global-shortcuts.ts index 825b062..7dfdc03 100644 --- a/src/main/runtime/global-shortcuts.ts +++ b/src/main/runtime/global-shortcuts.ts @@ -5,10 +5,7 @@ import type { RegisterGlobalShortcutsServiceOptions } from '../../core/services/ export function createGetConfiguredShortcutsHandler(deps: { getResolvedConfig: () => Config; defaultConfig: Config; - resolveConfiguredShortcuts: ( - config: Config, - defaultConfig: Config, - ) => ConfiguredShortcuts; + resolveConfiguredShortcuts: (config: Config, defaultConfig: Config) => ConfiguredShortcuts; }) { return (): ConfiguredShortcuts => deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig); diff --git a/src/main/runtime/immersion-startup-main-deps.test.ts b/src/main/runtime/immersion-startup-main-deps.test.ts index 45ffb05..2fbb077 100644 --- a/src/main/runtime/immersion-startup-main-deps.test.ts +++ b/src/main/runtime/immersion-startup-main-deps.test.ts @@ -5,7 +5,7 @@ import { createBuildImmersionTrackerStartupMainDepsHandler } from './immersion-s test('immersion tracker startup main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildImmersionTrackerStartupMainDepsHandler({ - getResolvedConfig: () => ({ immersionTracking: { enabled: true } } as never), + getResolvedConfig: () => ({ immersionTracking: { enabled: true } }) as never, getConfiguredDbPath: () => '/tmp/immersion.db', createTrackerService: () => { calls.push('create'); @@ -21,9 +21,12 @@ test('immersion tracker startup main deps builder maps callbacks', () => { assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } }); assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db'); - assert.deepEqual(deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), { - id: 'tracker', - }); + assert.deepEqual( + deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), + { + id: 'tracker', + }, + ); deps.setTracker(null); assert.equal(deps.getMpvClient()?.connected, true); deps.seedTrackerFromCurrentMedia(); diff --git a/src/main/runtime/initial-args-handler.test.ts b/src/main/runtime/initial-args-handler.test.ts index d01d648..ef836c0 100644 --- a/src/main/runtime/initial-args-handler.test.ts +++ b/src/main/runtime/initial-args-handler.test.ts @@ -24,7 +24,7 @@ test('initial args handler no-ops without initial args', () => { test('initial args handler ensures tray in background mode', () => { let ensuredTray = false; const handleInitialArgs = createHandleInitialArgsHandler({ - getInitialArgs: () => ({ start: true } as never), + getInitialArgs: () => ({ start: true }) as never, isBackgroundMode: () => true, ensureTray: () => { ensuredTray = true; @@ -44,7 +44,7 @@ test('initial args handler auto-connects mpv when needed', () => { let connectCalls = 0; let logged = false; const handleInitialArgs = createHandleInitialArgsHandler({ - getInitialArgs: () => ({ start: true } as never), + getInitialArgs: () => ({ start: true }) as never, isBackgroundMode: () => false, ensureTray: () => {}, isTexthookerOnlyMode: () => false, @@ -69,7 +69,7 @@ test('initial args handler auto-connects mpv when needed', () => { test('initial args handler forwards args to cli handler', () => { const seenSources: string[] = []; const handleInitialArgs = createHandleInitialArgsHandler({ - getInitialArgs: () => ({ start: true } as never), + getInitialArgs: () => ({ start: true }) as never, isBackgroundMode: () => false, ensureTray: () => {}, isTexthookerOnlyMode: () => false, diff --git a/src/main/runtime/initial-args-runtime-handler.test.ts b/src/main/runtime/initial-args-runtime-handler.test.ts index 6b764c8..44d9242 100644 --- a/src/main/runtime/initial-args-runtime-handler.test.ts +++ b/src/main/runtime/initial-args-runtime-handler.test.ts @@ -5,7 +5,7 @@ import { createInitialArgsRuntimeHandler } from './initial-args-runtime-handler' test('initial args runtime handler composes main deps and runs initial command flow', () => { const calls: string[] = []; const handleInitialArgs = createInitialArgsRuntimeHandler({ - getInitialArgs: () => ({ start: true } as never), + getInitialArgs: () => ({ start: true }) as never, isBackgroundMode: () => true, ensureTray: () => calls.push('tray'), isTexthookerOnlyMode: () => false, @@ -20,5 +20,10 @@ test('initial args runtime handler composes main deps and runs initial command f handleInitialArgs(); - assert.deepEqual(calls, ['tray', 'log:Auto-connecting MPV client for immersion tracking', 'connect', 'cli:initial']); + assert.deepEqual(calls, [ + 'tray', + 'log:Auto-connecting MPV client for immersion tracking', + 'connect', + 'cli:initial', + ]); }); diff --git a/src/main/runtime/ipc-mpv-command-main-deps.ts b/src/main/runtime/ipc-mpv-command-main-deps.ts index aed6a2b..8c62bf8 100644 --- a/src/main/runtime/ipc-mpv-command-main-deps.ts +++ b/src/main/runtime/ipc-mpv-command-main-deps.ts @@ -1,6 +1,8 @@ import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command'; -export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(deps: MpvCommandFromIpcRuntimeDeps) { +export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler( + deps: MpvCommandFromIpcRuntimeDeps, +) { return (): MpvCommandFromIpcRuntimeDeps => ({ triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(), diff --git a/src/main/runtime/ipc-runtime-handlers.ts b/src/main/runtime/ipc-runtime-handlers.ts index 74c50a2..867b057 100644 --- a/src/main/runtime/ipc-runtime-handlers.ts +++ b/src/main/runtime/ipc-runtime-handlers.ts @@ -1,4 +1,7 @@ -import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler } from './ipc-bridge-actions'; +import { + createHandleMpvCommandFromIpcHandler, + createRunSubsyncManualFromIpcHandler, +} from './ipc-bridge-actions'; import { createBuildHandleMpvCommandFromIpcMainDepsHandler, createBuildRunSubsyncManualFromIpcMainDepsHandler, @@ -22,10 +25,10 @@ export function createIpcRuntimeHandlers(deps: { handleMpvCommandFromIpcMainDeps, ); - const runSubsyncManualFromIpcMainDeps = - createBuildRunSubsyncManualFromIpcMainDepsHandler( - deps.runSubsyncManualFromIpcDeps, - )(); + const runSubsyncManualFromIpcMainDeps = createBuildRunSubsyncManualFromIpcMainDepsHandler< + TRequest, + TResult + >(deps.runSubsyncManualFromIpcDeps)(); const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler( runSubsyncManualFromIpcMainDeps, ); diff --git a/src/main/runtime/jellyfin-cli-list.ts b/src/main/runtime/jellyfin-cli-list.ts index 949cd48..b711b3c 100644 --- a/src/main/runtime/jellyfin-cli-list.ts +++ b/src/main/runtime/jellyfin-cli-list.ts @@ -94,7 +94,11 @@ export function createHandleJellyfinListCommands(deps: { if (!args.jellyfinItemId) { throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.'); } - const tracks = await deps.listJellyfinSubtitleTracks(session, clientInfo, args.jellyfinItemId); + const tracks = await deps.listJellyfinSubtitleTracks( + session, + clientInfo, + args.jellyfinItemId, + ); if (tracks.length === 0) { deps.logInfo('No Jellyfin subtitle tracks found for item.'); return true; diff --git a/src/main/runtime/jellyfin-cli-main-deps.ts b/src/main/runtime/jellyfin-cli-main-deps.ts index 41042a2..5f85d97 100644 --- a/src/main/runtime/jellyfin-cli-main-deps.ts +++ b/src/main/runtime/jellyfin-cli-main-deps.ts @@ -1,15 +1,7 @@ -import type { - createHandleJellyfinAuthCommands, -} from './jellyfin-cli-auth'; -import type { - createHandleJellyfinListCommands, -} from './jellyfin-cli-list'; -import type { - createHandleJellyfinPlayCommand, -} from './jellyfin-cli-play'; -import type { - createHandleJellyfinRemoteAnnounceCommand, -} from './jellyfin-cli-remote-announce'; +import type { createHandleJellyfinAuthCommands } from './jellyfin-cli-auth'; +import type { createHandleJellyfinListCommands } from './jellyfin-cli-list'; +import type { createHandleJellyfinPlayCommand } from './jellyfin-cli-play'; +import type { createHandleJellyfinRemoteAnnounceCommand } from './jellyfin-cli-remote-announce'; type HandleJellyfinAuthCommandsMainDeps = Parameters[0]; type HandleJellyfinListCommandsMainDeps = Parameters[0]; diff --git a/src/main/runtime/jellyfin-client-info-main-deps.ts b/src/main/runtime/jellyfin-client-info-main-deps.ts index 2bc1d1e..343ee21 100644 --- a/src/main/runtime/jellyfin-client-info-main-deps.ts +++ b/src/main/runtime/jellyfin-client-info-main-deps.ts @@ -3,7 +3,9 @@ import type { createGetResolvedJellyfinConfigHandler, } from './jellyfin-client-info'; -type GetResolvedJellyfinConfigMainDeps = Parameters[0]; +type GetResolvedJellyfinConfigMainDeps = Parameters< + typeof createGetResolvedJellyfinConfigHandler +>[0]; type GetJellyfinClientInfoMainDeps = Parameters[0]; export function createBuildGetResolvedJellyfinConfigMainDepsHandler( diff --git a/src/main/runtime/jellyfin-client-info.test.ts b/src/main/runtime/jellyfin-client-info.test.ts index 135aa93..08ffc5a 100644 --- a/src/main/runtime/jellyfin-client-info.test.ts +++ b/src/main/runtime/jellyfin-client-info.test.ts @@ -8,7 +8,7 @@ import { test('get resolved jellyfin config returns jellyfin section from resolved config', () => { const jellyfin = { url: 'https://jellyfin.local' } as never; const getConfig = createGetResolvedJellyfinConfigHandler({ - getResolvedConfig: () => ({ jellyfin } as never), + getResolvedConfig: () => ({ jellyfin }) as never, loadStoredSession: () => null, getEnv: () => undefined, }); @@ -68,8 +68,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou }, }) as never, loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }), - getEnv: (key: string) => - key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined, + getEnv: (key: string) => (key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined), }); assert.deepEqual(getConfig(), { @@ -81,7 +80,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou test('jellyfin client info resolves defaults when fields are missing', () => { const getClientInfo = createGetJellyfinClientInfoHandler({ - getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never), + getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' }) as never, getDefaultJellyfinConfig: () => ({ clientName: 'SubMiner', diff --git a/src/main/runtime/jellyfin-playback-launch-main-deps.ts b/src/main/runtime/jellyfin-playback-launch-main-deps.ts index 83af3c1..a0d1525 100644 --- a/src/main/runtime/jellyfin-playback-launch-main-deps.ts +++ b/src/main/runtime/jellyfin-playback-launch-main-deps.ts @@ -2,7 +2,9 @@ import type { createPlayJellyfinItemInMpvHandler } from './jellyfin-playback-lau type PlayJellyfinItemInMpvMainDeps = Parameters[0]; -export function createBuildPlayJellyfinItemInMpvMainDepsHandler(deps: PlayJellyfinItemInMpvMainDeps) { +export function createBuildPlayJellyfinItemInMpvMainDepsHandler( + deps: PlayJellyfinItemInMpvMainDeps, +) { return (): PlayJellyfinItemInMpvMainDeps => ({ ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(), getMpvClient: () => deps.getMpvClient(), diff --git a/src/main/runtime/jellyfin-remote-commands.test.ts b/src/main/runtime/jellyfin-remote-commands.test.ts index 313545e..b3586db 100644 --- a/src/main/runtime/jellyfin-remote-commands.test.ts +++ b/src/main/runtime/jellyfin-remote-commands.test.ts @@ -136,6 +136,8 @@ test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices', assert.equal(playback.subtitleStreamIndex, null); assert.ok(calls.includes('progress:true')); assert.ok( - calls.some((entry) => entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand')), + calls.some((entry) => + entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand'), + ), ); }); diff --git a/src/main/runtime/jellyfin-remote-playback.ts b/src/main/runtime/jellyfin-remote-playback.ts index e39ffec..f085ef5 100644 --- a/src/main/runtime/jellyfin-remote-playback.ts +++ b/src/main/runtime/jellyfin-remote-playback.ts @@ -44,7 +44,9 @@ export type JellyfinRemoteProgressReporterDeps = { logDebug: (message: string, error: unknown) => void; }; -export function createReportJellyfinRemoteProgressHandler(deps: JellyfinRemoteProgressReporterDeps) { +export function createReportJellyfinRemoteProgressHandler( + deps: JellyfinRemoteProgressReporterDeps, +) { return async (force = false): Promise => { const playback = deps.getActivePlayback(); if (!playback) return; diff --git a/src/main/runtime/jellyfin-remote-session-main-deps.ts b/src/main/runtime/jellyfin-remote-session-main-deps.ts index 657f583..49b2e15 100644 --- a/src/main/runtime/jellyfin-remote-session-main-deps.ts +++ b/src/main/runtime/jellyfin-remote-session-main-deps.ts @@ -3,8 +3,12 @@ import type { createStopJellyfinRemoteSessionHandler, } from './jellyfin-remote-session-lifecycle'; -type StartJellyfinRemoteSessionMainDeps = Parameters[0]; -type StopJellyfinRemoteSessionMainDeps = Parameters[0]; +type StartJellyfinRemoteSessionMainDeps = Parameters< + typeof createStartJellyfinRemoteSessionHandler +>[0]; +type StopJellyfinRemoteSessionMainDeps = Parameters< + typeof createStopJellyfinRemoteSessionHandler +>[0]; export function createBuildStartJellyfinRemoteSessionMainDepsHandler( deps: StartJellyfinRemoteSessionMainDeps, diff --git a/src/main/runtime/jellyfin-setup-window-main-deps.test.ts b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts index aa3107e..0c09b39 100644 --- a/src/main/runtime/jellyfin-setup-window-main-deps.test.ts +++ b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts @@ -16,7 +16,11 @@ test('open jellyfin setup window main deps builder maps callbacks', async () => accessToken: 'token', userId: 'uid', }), - getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }), + getJellyfinClientInfo: () => ({ + clientName: 'SubMiner', + clientVersion: '1.0', + deviceId: 'dev', + }), saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: (message) => calls.push(`info:${message}`), @@ -38,12 +42,15 @@ test('open jellyfin setup window main deps builder maps callbacks', async () => username: 'u', password: 'p', }); - assert.deepEqual(await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), { - serverUrl: 'http://127.0.0.1:8096', - username: 'alice', - accessToken: 'token', - userId: 'uid', - }); + assert.deepEqual( + await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), + { + serverUrl: 'http://127.0.0.1:8096', + username: 'alice', + accessToken: 'token', + userId: 'uid', + }, + ); deps.saveStoredSession({ accessToken: 'token', userId: 'uid' }); deps.patchJellyfinConfig({ serverUrl: 'http://127.0.0.1:8096', @@ -57,5 +64,13 @@ test('open jellyfin setup window main deps builder maps callbacks', async () => deps.clearSetupWindow(); deps.setSetupWindow({} as never); assert.equal(deps.encodeURIComponent('a b'), 'a%20b'); - assert.deepEqual(calls, ['save', 'patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']); + assert.deepEqual(calls, [ + 'save', + 'patch', + 'info:ok', + 'error:bad', + 'osd:toast', + 'clear', + 'set-window', + ]); }); diff --git a/src/main/runtime/jellyfin-setup-window.test.ts b/src/main/runtime/jellyfin-setup-window.test.ts index b87cfd6..77331dc 100644 --- a/src/main/runtime/jellyfin-setup-window.test.ts +++ b/src/main/runtime/jellyfin-setup-window.test.ts @@ -50,7 +50,11 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn accessToken: 'token', userId: 'uid', }), - getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), + getJellyfinClientInfo: () => ({ + clientName: 'SubMiner', + clientVersion: '1.0', + deviceId: 'did', + }), saveStoredSession: (session) => { savedSession = session; calls.push('save'); @@ -86,7 +90,11 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async authenticateWithPassword: async () => { throw new Error('bad credentials'); }, - getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), + getJellyfinClientInfo: () => ({ + clientName: 'SubMiner', + clientVersion: '1.0', + deviceId: 'did', + }), saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: () => calls.push('info'), @@ -180,7 +188,11 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is authenticateWithPassword: async () => { throw new Error('should not auth'); }, - getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), + getJellyfinClientInfo: () => ({ + clientName: 'SubMiner', + clientVersion: '1.0', + deviceId: 'did', + }), saveStoredSession: () => {}, patchJellyfinConfig: () => {}, logInfo: () => {}, @@ -196,14 +208,18 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is }); test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => { - let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null; + let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = + null; let closedHandler: (() => void) | null = null; let prevented = false; const calls: string[] = []; const fakeWindow = { focus: () => {}, webContents: { - on: (event: 'will-navigate', handler: (event: { preventDefault: () => void }, url: string) => void) => { + on: ( + event: 'will-navigate', + handler: (event: { preventDefault: () => void }, url: string) => void, + ) => { if (event === 'will-navigate') { willNavigateHandler = handler; } @@ -233,7 +249,11 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li accessToken: 'token', userId: 'uid', }), - getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }), + getJellyfinClientInfo: () => ({ + clientName: 'SubMiner', + clientVersion: '1.0', + deviceId: 'did', + }), saveStoredSession: () => calls.push('save'), patchJellyfinConfig: () => calls.push('patch'), logInfo: () => calls.push('info'), @@ -249,7 +269,9 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li assert.ok(closedHandler); assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']); - const navHandler = willNavigateHandler as ((event: { preventDefault: () => void }, url: string) => void) | null; + const navHandler = willNavigateHandler as + | ((event: { preventDefault: () => void }, url: string) => void) + | null; if (!navHandler) { throw new Error('missing will-navigate handler'); } diff --git a/src/main/runtime/jellyfin-setup-window.ts b/src/main/runtime/jellyfin-setup-window.ts index 568ead4..604bc0f 100644 --- a/src/main/runtime/jellyfin-setup-window.ts +++ b/src/main/runtime/jellyfin-setup-window.ts @@ -109,7 +109,9 @@ export function parseJellyfinSetupSubmissionUrl(rawUrl: string): { } export function createHandleJellyfinSetupSubmissionHandler(deps: { - parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null; + parseSubmissionUrl: ( + rawUrl: string, + ) => { server: string; username: string; password: string } | null; authenticateWithPassword: ( server: string, username: string, @@ -179,20 +181,22 @@ export function createHandleJellyfinSetupWindowClosedHandler(deps: { }; } -export function createHandleJellyfinSetupWindowOpenedHandler(deps: { - setSetupWindow: () => void; -}) { +export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) { return (): void => { deps.setSetupWindow(); }; } -export function createOpenJellyfinSetupWindowHandler(deps: { +export function createOpenJellyfinSetupWindowHandler< + TWindow extends JellyfinSetupWindowLike, +>(deps: { maybeFocusExistingSetupWindow: () => boolean; createSetupWindow: () => TWindow; getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null }; buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string; - parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null; + parseSubmissionUrl: ( + rawUrl: string, + ) => { server: string; username: string; password: string } | null; authenticateWithPassword: ( server: string, username: string, @@ -258,9 +262,7 @@ export function createOpenJellyfinSetupWindowHandler { handleWindowClosed(); }); diff --git a/src/main/runtime/jellyfin-subtitle-preload.ts b/src/main/runtime/jellyfin-subtitle-preload.ts index beb2e5f..409dff5 100644 --- a/src/main/runtime/jellyfin-subtitle-preload.ts +++ b/src/main/runtime/jellyfin-subtitle-preload.ts @@ -117,13 +117,7 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: { seenUrls.add(track.deliveryUrl); const labelBase = (track.title || track.language || '').trim(); const label = labelBase || `Jellyfin Subtitle ${track.index}`; - deps.sendMpvCommand([ - 'sub-add', - track.deliveryUrl, - 'cached', - label, - track.language || '', - ]); + deps.sendMpvCommand(['sub-add', track.deliveryUrl, 'cached', label, track.language || '']); } await deps.wait(250); diff --git a/src/main/runtime/mining-actions.ts b/src/main/runtime/mining-actions.ts index 7e0c4c4..9ee1935 100644 --- a/src/main/runtime/mining-actions.ts +++ b/src/main/runtime/mining-actions.ts @@ -39,27 +39,28 @@ export function createCopyCurrentSubtitleHandler(deps: { }; } -export function createHandleMineSentenceDigitHandler( - deps: { - getSubtitleTimingTracker: () => TSubtitleTimingTracker; - getAnkiIntegration: () => TAnkiIntegration; - getCurrentSecondarySubText: () => string | undefined; - showMpvOsd: (text: string) => void; - logError: (message: string, err: unknown) => void; - onCardsMined: (count: number) => void; - handleMineSentenceDigitCore: ( - count: number, - options: { - subtitleTimingTracker: TSubtitleTimingTracker; - ankiIntegration: TAnkiIntegration; - getCurrentSecondarySubText: () => string | undefined; - showMpvOsd: (text: string) => void; - logError: (message: string, err: unknown) => void; - onCardsMined: (count: number) => void; - }, - ) => void; - }, -) { +export function createHandleMineSentenceDigitHandler< + TSubtitleTimingTracker, + TAnkiIntegration, +>(deps: { + getSubtitleTimingTracker: () => TSubtitleTimingTracker; + getAnkiIntegration: () => TAnkiIntegration; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + onCardsMined: (count: number) => void; + handleMineSentenceDigitCore: ( + count: number, + options: { + subtitleTimingTracker: TSubtitleTimingTracker; + ankiIntegration: TAnkiIntegration; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + onCardsMined: (count: number) => void; + }, + ) => void; +}) { return (count: number): void => { deps.handleMineSentenceDigitCore(count, { subtitleTimingTracker: deps.getSubtitleTimingTracker(), diff --git a/src/main/runtime/mpv-client-runtime-service-main-deps.test.ts b/src/main/runtime/mpv-client-runtime-service-main-deps.test.ts index 95aa2fe..0ffdc80 100644 --- a/src/main/runtime/mpv-client-runtime-service-main-deps.test.ts +++ b/src/main/runtime/mpv-client-runtime-service-main-deps.test.ts @@ -7,7 +7,10 @@ test('mpv runtime service main deps builder maps state and callbacks', () => { let reconnectTimer: ReturnType | null = null; class FakeClient { - constructor(public socketPath: string, public options: unknown) {} + constructor( + public socketPath: string, + public options: unknown, + ) {} } const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({ diff --git a/src/main/runtime/mpv-client-runtime-service-main-deps.ts b/src/main/runtime/mpv-client-runtime-service-main-deps.ts index d11ad7b..c3c4fb2 100644 --- a/src/main/runtime/mpv-client-runtime-service-main-deps.ts +++ b/src/main/runtime/mpv-client-runtime-service-main-deps.ts @@ -22,7 +22,8 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler< setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible), isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(), getReconnectTimer: () => deps.getReconnectTimer(), - setReconnectTimer: (timer: ReturnType | null) => deps.setReconnectTimer(timer), + setReconnectTimer: (timer: ReturnType | null) => + deps.setReconnectTimer(timer), }, bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client), }); diff --git a/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts b/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts index f79624e..d2ca031 100644 --- a/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts +++ b/src/main/runtime/mpv-jellyfin-defaults-main-deps.ts @@ -15,9 +15,7 @@ export function createBuildApplyJellyfinMpvDefaultsMainDepsHandler( }); } -export function createBuildGetDefaultSocketPathMainDepsHandler( - deps: GetDefaultSocketPathMainDeps, -) { +export function createBuildGetDefaultSocketPathMainDepsHandler(deps: GetDefaultSocketPathMainDeps) { return (): GetDefaultSocketPathMainDeps => ({ platform: deps.platform, }); diff --git a/src/main/runtime/mpv-main-event-bindings.ts b/src/main/runtime/mpv-main-event-bindings.ts index 9e50601..2d10a9f 100644 --- a/src/main/runtime/mpv-main-event-bindings.ts +++ b/src/main/runtime/mpv-main-event-bindings.ts @@ -97,8 +97,7 @@ export function createBindMpvMainEventHandlersHandler(deps: { const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({ updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path), reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(), - restoreMpvSubVisibility: () => - deps.restoreMpvSubVisibility(), + restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(), getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(), resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey), maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey), diff --git a/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts index a48235d..fffb68d 100644 --- a/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts +++ b/src/main/runtime/mpv-subtitle-render-metrics-main-deps.ts @@ -1,6 +1,8 @@ import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics'; -type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters[0]; +type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters< + typeof createUpdateMpvSubtitleRenderMetricsHandler +>[0]; export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler( deps: UpdateMpvSubtitleRenderMetricsMainDeps, diff --git a/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts b/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts index f9cca84..d19c579 100644 --- a/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts +++ b/src/main/runtime/numeric-shortcut-runtime-main-deps.test.ts @@ -22,7 +22,10 @@ test('numeric shortcut runtime main deps builder maps callbacks', () => { }, })(); - assert.equal(deps.globalShortcut.register('1', () => {}), true); + assert.equal( + deps.globalShortcut.register('1', () => {}), + true, + ); deps.globalShortcut.unregister('1'); deps.showMpvOsd('x'); deps.setTimer(() => calls.push('tick'), 1000); diff --git a/src/main/runtime/numeric-shortcut-runtime-main-deps.ts b/src/main/runtime/numeric-shortcut-runtime-main-deps.ts index d28f306..d099cbe 100644 --- a/src/main/runtime/numeric-shortcut-runtime-main-deps.ts +++ b/src/main/runtime/numeric-shortcut-runtime-main-deps.ts @@ -1,6 +1,8 @@ import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut'; -export function createBuildNumericShortcutRuntimeMainDepsHandler(deps: NumericShortcutRuntimeOptions) { +export function createBuildNumericShortcutRuntimeMainDepsHandler( + deps: NumericShortcutRuntimeOptions, +) { return (): NumericShortcutRuntimeOptions => ({ globalShortcut: deps.globalShortcut, showMpvOsd: (text: string) => deps.showMpvOsd(text), diff --git a/src/main/runtime/numeric-shortcut-session-main-deps.ts b/src/main/runtime/numeric-shortcut-session-main-deps.ts index 1970d9c..d2ecd65 100644 --- a/src/main/runtime/numeric-shortcut-session-main-deps.ts +++ b/src/main/runtime/numeric-shortcut-session-main-deps.ts @@ -3,8 +3,12 @@ import type { createStartNumericShortcutSessionHandler, } from './numeric-shortcut-session-handlers'; -type CancelNumericShortcutSessionMainDeps = Parameters[0]; -type StartNumericShortcutSessionMainDeps = Parameters[0]; +type CancelNumericShortcutSessionMainDeps = Parameters< + typeof createCancelNumericShortcutSessionHandler +>[0]; +type StartNumericShortcutSessionMainDeps = Parameters< + typeof createStartNumericShortcutSessionHandler +>[0]; export function createBuildCancelNumericShortcutSessionMainDepsHandler( deps: CancelNumericShortcutSessionMainDeps, diff --git a/src/main/runtime/numeric-shortcut-session-runtime-handlers.ts b/src/main/runtime/numeric-shortcut-session-runtime-handlers.ts index c0c098f..946ea77 100644 --- a/src/main/runtime/numeric-shortcut-session-runtime-handlers.ts +++ b/src/main/runtime/numeric-shortcut-session-runtime-handlers.ts @@ -20,8 +20,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: { const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({ session: deps.multiCopySession, })(); - const cancelPendingMultiCopyHandler = - createCancelNumericShortcutSessionHandler(cancelPendingMultiCopyMainDeps); + const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler( + cancelPendingMultiCopyMainDeps, + ); const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({ session: deps.multiCopySession, @@ -32,8 +33,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: { cancelled: 'Cancelled', }, })(); - const startPendingMultiCopyHandler = - createStartNumericShortcutSessionHandler(startPendingMultiCopyMainDeps); + const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler( + startPendingMultiCopyMainDeps, + ); const cancelPendingMineSentenceMultipleMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({ diff --git a/src/main/runtime/overlay-bootstrap-main-deps.ts b/src/main/runtime/overlay-bootstrap-main-deps.ts index b16d6c5..ec74f9f 100644 --- a/src/main/runtime/overlay-bootstrap-main-deps.ts +++ b/src/main/runtime/overlay-bootstrap-main-deps.ts @@ -14,9 +14,7 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler( }); } -export function createBuildOverlayModalRuntimeMainDepsHandler( - deps: OverlayWindowResolver, -) { +export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) { return (): OverlayWindowResolver => ({ getMainWindow: () => deps.getMainWindow(), getModalWindow: () => deps.getModalWindow(), diff --git a/src/main/runtime/overlay-main-actions-main-deps.test.ts b/src/main/runtime/overlay-main-actions-main-deps.test.ts index c9731fa..ff0af18 100644 --- a/src/main/runtime/overlay-main-actions-main-deps.test.ts +++ b/src/main/runtime/overlay-main-actions-main-deps.test.ts @@ -35,12 +35,15 @@ test('overlay main action main deps builders map callbacks', () => { showMpvOsd: (text) => calls.push(`osd:${text}`), sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`), })(); - assert.deepEqual(append.appendClipboardVideoToQueueRuntime({ - getMpvClient: () => ({ connected: true }), - readClipboardText: () => '/tmp/v.mkv', - showMpvOsd: () => {}, - sendMpvCommand: () => {}, - }), { ok: true, message: 'ok' }); + assert.deepEqual( + append.appendClipboardVideoToQueueRuntime({ + getMpvClient: () => ({ connected: true }), + readClipboardText: () => '/tmp/v.mkv', + showMpvOsd: () => {}, + sendMpvCommand: () => {}, + }), + { ok: true, message: 'ok' }, + ); assert.equal(append.readClipboardText(), '/tmp/v.mkv'); assert.equal(typeof append.getMpvClient(), 'object'); append.showMpvOsd('queued'); diff --git a/src/main/runtime/overlay-main-actions-main-deps.ts b/src/main/runtime/overlay-main-actions-main-deps.ts index 387c6d4..94edc6b 100644 --- a/src/main/runtime/overlay-main-actions-main-deps.ts +++ b/src/main/runtime/overlay-main-actions-main-deps.ts @@ -8,7 +8,9 @@ import type { type SetOverlayVisibleMainDeps = Parameters[0]; type ToggleOverlayMainDeps = Parameters[0]; type HandleOverlayModalClosedMainDeps = Parameters[0]; -type AppendClipboardVideoToQueueMainDeps = Parameters[0]; +type AppendClipboardVideoToQueueMainDeps = Parameters< + typeof createAppendClipboardVideoToQueueHandler +>[0]; export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) { return (): SetOverlayVisibleMainDeps => ({ @@ -34,7 +36,8 @@ export function createBuildAppendClipboardVideoToQueueMainDepsHandler( deps: AppendClipboardVideoToQueueMainDeps, ) { return (): AppendClipboardVideoToQueueMainDeps => ({ - appendClipboardVideoToQueueRuntime: (options) => deps.appendClipboardVideoToQueueRuntime(options), + appendClipboardVideoToQueueRuntime: (options) => + deps.appendClipboardVideoToQueueRuntime(options), getMpvClient: () => deps.getMpvClient(), readClipboardText: () => deps.readClipboardText(), showMpvOsd: (text: string) => deps.showMpvOsd(text), diff --git a/src/main/runtime/overlay-main-actions.ts b/src/main/runtime/overlay-main-actions.ts index fc0b2a2..e7fe1af 100644 --- a/src/main/runtime/overlay-main-actions.ts +++ b/src/main/runtime/overlay-main-actions.ts @@ -9,9 +9,7 @@ export function createSetOverlayVisibleHandler(deps: { }; } -export function createToggleOverlayHandler(deps: { - toggleVisibleOverlay: () => void; -}) { +export function createToggleOverlayHandler(deps: { toggleVisibleOverlay: () => void }) { return (): void => { deps.toggleVisibleOverlay(); }; @@ -26,9 +24,10 @@ export function createHandleOverlayModalClosedHandler(deps: { } export function createAppendClipboardVideoToQueueHandler(deps: { - appendClipboardVideoToQueueRuntime: ( - options: AppendClipboardVideoToQueueRuntimeDeps, - ) => { ok: boolean; message: string }; + appendClipboardVideoToQueueRuntime: (options: AppendClipboardVideoToQueueRuntimeDeps) => { + ok: boolean; + message: string; + }; getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T ? T : never; diff --git a/src/main/runtime/overlay-mpv-sub-visibility.ts b/src/main/runtime/overlay-mpv-sub-visibility.ts index ec994e6..1640834 100644 --- a/src/main/runtime/overlay-mpv-sub-visibility.ts +++ b/src/main/runtime/overlay-mpv-sub-visibility.ts @@ -25,7 +25,6 @@ function parseSubVisibility(value: unknown): boolean { return true; } - export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: { getMpvClient: () => MpvVisibilityClient | null; getSavedSubVisibility: () => boolean | null; diff --git a/src/main/runtime/overlay-runtime-bootstrap.test.ts b/src/main/runtime/overlay-runtime-bootstrap.test.ts index 82256d7..d428dd6 100644 --- a/src/main/runtime/overlay-runtime-bootstrap.test.ts +++ b/src/main/runtime/overlay-runtime-bootstrap.test.ts @@ -9,7 +9,7 @@ test('overlay runtime bootstrap no-ops when already initialized', () => { initializeOverlayRuntimeCore: () => { coreCalls += 1; }, - buildOptions: () => ({} as never), + buildOptions: () => ({}) as never, setOverlayRuntimeInitialized: () => {}, startBackgroundWarmups: () => {}, }); diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts index 298e805..a670648 100644 --- a/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.test.ts @@ -32,7 +32,10 @@ test('broadcast runtime options changed main deps builder maps callbacks', () => broadcastToOverlayWindows: (channel) => calls.push(channel), })(); - deps.broadcastRuntimeOptionsChangedRuntime(() => [], () => {}); + deps.broadcastRuntimeOptionsChangedRuntime( + () => [], + () => {}, + ); deps.broadcastToOverlayWindows('runtime-options:changed'); assert.deepEqual(deps.getRuntimeOptionsState(), []); assert.deepEqual(calls, ['broadcast-runtime', 'runtime-options:changed']); diff --git a/src/main/runtime/overlay-runtime-main-actions-main-deps.ts b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts index 23dd597..9076685 100644 --- a/src/main/runtime/overlay-runtime-main-actions-main-deps.ts +++ b/src/main/runtime/overlay-runtime-main-actions-main-deps.ts @@ -14,11 +14,15 @@ type RestorePreviousSecondarySubVisibilityMainDeps = Parameters< type BroadcastRuntimeOptionsChangedMainDeps = Parameters< typeof createBroadcastRuntimeOptionsChangedHandler >[0]; -type SendToActiveOverlayWindowMainDeps = Parameters[0]; +type SendToActiveOverlayWindowMainDeps = Parameters< + typeof createSendToActiveOverlayWindowHandler +>[0]; type SetOverlayDebugVisualizationEnabledMainDeps = Parameters< typeof createSetOverlayDebugVisualizationEnabledHandler >[0]; -type OpenRuntimeOptionsPaletteMainDeps = Parameters[0]; +type OpenRuntimeOptionsPaletteMainDeps = Parameters< + typeof createOpenRuntimeOptionsPaletteHandler +>[0]; export function createBuildGetRuntimeOptionsStateMainDepsHandler( deps: GetRuntimeOptionsStateMainDeps, @@ -61,11 +65,7 @@ export function createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler( deps: SetOverlayDebugVisualizationEnabledMainDeps, ) { return (): SetOverlayDebugVisualizationEnabledMainDeps => ({ - setOverlayDebugVisualizationEnabledRuntime: ( - currentEnabled, - nextEnabled, - setCurrentEnabled, - ) => + setOverlayDebugVisualizationEnabledRuntime: (currentEnabled, nextEnabled, setCurrentEnabled) => deps.setOverlayDebugVisualizationEnabledRuntime( currentEnabled, nextEnabled, diff --git a/src/main/runtime/overlay-runtime-main-actions.test.ts b/src/main/runtime/overlay-runtime-main-actions.test.ts index 109613e..7937672 100644 --- a/src/main/runtime/overlay-runtime-main-actions.test.ts +++ b/src/main/runtime/overlay-runtime-main-actions.test.ts @@ -49,7 +49,10 @@ test('runtime options state handler returns list from manager', () => { test('restore previous secondary subtitle visibility no-ops without connected mpv client', () => { let restored = false; const restore = createRestorePreviousSecondarySubVisibilityHandler({ - getMpvClient: () => ({ connected: false, restorePreviousSecondarySubVisibility: () => (restored = true) }), + getMpvClient: () => ({ + connected: false, + restorePreviousSecondarySubVisibility: () => (restored = true), + }), }); restore(); assert.equal(restored, false); @@ -58,7 +61,10 @@ test('restore previous secondary subtitle visibility no-ops without connected mp test('restore previous secondary subtitle visibility calls runtime when connected', () => { let restored = false; const restore = createRestorePreviousSecondarySubVisibilityHandler({ - getMpvClient: () => ({ connected: true, restorePreviousSecondarySubVisibility: () => (restored = true) }), + getMpvClient: () => ({ + connected: true, + restorePreviousSecondarySubVisibility: () => (restored = true), + }), }); restore(); assert.equal(restored, true); @@ -82,7 +88,8 @@ test('broadcast runtime options changed passes through state getter and broadcas requiresRestart: false, }, ], - broadcastToOverlayWindows: (channel, payload) => calls.push(`emit:${channel}:${JSON.stringify(payload)}`), + broadcastToOverlayWindows: (channel, payload) => + calls.push(`emit:${channel}:${JSON.stringify(payload)}`), }); broadcast(); diff --git a/src/main/runtime/overlay-runtime-main-actions.ts b/src/main/runtime/overlay-runtime-main-actions.ts index db70b3b..108921a 100644 --- a/src/main/runtime/overlay-runtime-main-actions.ts +++ b/src/main/runtime/overlay-runtime-main-actions.ts @@ -70,10 +70,8 @@ export function createSetOverlayDebugVisualizationEnabledHandler(deps: { setCurrentEnabled: (enabled: boolean) => void; }) { return (enabled: boolean): void => { - deps.setOverlayDebugVisualizationEnabledRuntime( - deps.getCurrentEnabled(), - enabled, - (next) => deps.setCurrentEnabled(next), + deps.setOverlayDebugVisualizationEnabledRuntime(deps.getCurrentEnabled(), enabled, (next) => + deps.setCurrentEnabled(next), ); }; } diff --git a/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts index 25814ac..41a207b 100644 --- a/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts +++ b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts @@ -6,7 +6,9 @@ import type { } from './overlay-shortcuts-lifecycle'; type RegisterOverlayShortcutsMainDeps = Parameters[0]; -type UnregisterOverlayShortcutsMainDeps = Parameters[0]; +type UnregisterOverlayShortcutsMainDeps = Parameters< + typeof createUnregisterOverlayShortcutsHandler +>[0]; type SyncOverlayShortcutsMainDeps = Parameters[0]; type RefreshOverlayShortcutsMainDeps = Parameters[0]; diff --git a/src/main/runtime/overlay-shortcuts-runtime-handlers.ts b/src/main/runtime/overlay-shortcuts-runtime-handlers.ts index 03624ff..365a462 100644 --- a/src/main/runtime/overlay-shortcuts-runtime-handlers.ts +++ b/src/main/runtime/overlay-shortcuts-runtime-handlers.ts @@ -21,26 +21,30 @@ export function createOverlayShortcutsRuntimeHandlers(deps: { const registerOverlayShortcutsMainDeps = createBuildRegisterOverlayShortcutsMainDepsHandler( deps.overlayShortcutsRuntimeMainDeps, )(); - const registerOverlayShortcutsHandler = - createRegisterOverlayShortcutsHandler(registerOverlayShortcutsMainDeps); + const registerOverlayShortcutsHandler = createRegisterOverlayShortcutsHandler( + registerOverlayShortcutsMainDeps, + ); - const unregisterOverlayShortcutsMainDeps = - createBuildUnregisterOverlayShortcutsMainDepsHandler( - deps.overlayShortcutsRuntimeMainDeps, - )(); - const unregisterOverlayShortcutsHandler = - createUnregisterOverlayShortcutsHandler(unregisterOverlayShortcutsMainDeps); + const unregisterOverlayShortcutsMainDeps = createBuildUnregisterOverlayShortcutsMainDepsHandler( + deps.overlayShortcutsRuntimeMainDeps, + )(); + const unregisterOverlayShortcutsHandler = createUnregisterOverlayShortcutsHandler( + unregisterOverlayShortcutsMainDeps, + ); const syncOverlayShortcutsMainDeps = createBuildSyncOverlayShortcutsMainDepsHandler( deps.overlayShortcutsRuntimeMainDeps, )(); - const syncOverlayShortcutsHandler = createSyncOverlayShortcutsHandler(syncOverlayShortcutsMainDeps); + const syncOverlayShortcutsHandler = createSyncOverlayShortcutsHandler( + syncOverlayShortcutsMainDeps, + ); const refreshOverlayShortcutsMainDeps = createBuildRefreshOverlayShortcutsMainDepsHandler( deps.overlayShortcutsRuntimeMainDeps, )(); - const refreshOverlayShortcutsHandler = - createRefreshOverlayShortcutsHandler(refreshOverlayShortcutsMainDeps); + const refreshOverlayShortcutsHandler = createRefreshOverlayShortcutsHandler( + refreshOverlayShortcutsMainDeps, + ); return { registerOverlayShortcuts: () => registerOverlayShortcutsHandler(), diff --git a/src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts b/src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts index f83c15c..aa14001 100644 --- a/src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts +++ b/src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts @@ -6,7 +6,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call const calls: string[] = []; let shortcutsRegistered = false; const deps = createBuildOverlayShortcutsRuntimeMainDepsHandler({ - getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never), + getConfiguredShortcuts: () => ({ copySubtitle: 's' }) as never, getShortcutsRegistered: () => shortcutsRegistered, setShortcutsRegistered: (registered) => { shortcutsRegistered = registered; diff --git a/src/main/runtime/overlay-visibility-actions-main-deps.ts b/src/main/runtime/overlay-visibility-actions-main-deps.ts index d46e2e9..df55471 100644 --- a/src/main/runtime/overlay-visibility-actions-main-deps.ts +++ b/src/main/runtime/overlay-visibility-actions-main-deps.ts @@ -11,7 +11,8 @@ export function createBuildSetVisibleOverlayVisibleMainDepsHandler( ) { return (): SetVisibleOverlayVisibleMainDeps => ({ setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options), - setVisibleOverlayVisibleState: (visible: boolean) => deps.setVisibleOverlayVisibleState(visible), + setVisibleOverlayVisibleState: (visible: boolean) => + deps.setVisibleOverlayVisibleState(visible), updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), onVisibleOverlayEnabled: deps.onVisibleOverlayEnabled, }); diff --git a/src/main/runtime/overlay-visibility-actions.test.ts b/src/main/runtime/overlay-visibility-actions.test.ts index 692f446..b109ee3 100644 --- a/src/main/runtime/overlay-visibility-actions.test.ts +++ b/src/main/runtime/overlay-visibility-actions.test.ts @@ -22,11 +22,7 @@ test('set visible overlay handler forwards dependencies to core', () => { }); setVisible(true); - assert.deepEqual(calls, [ - 'core:true', - 'state:true', - 'update-visible', - ]); + assert.deepEqual(calls, ['core:true', 'state:true', 'update-visible']); assert.equal(warmupStarts, 1); setVisible(false); diff --git a/src/main/runtime/overlay-visibility-runtime-main-deps.ts b/src/main/runtime/overlay-visibility-runtime-main-deps.ts index 85c996d..72c7024 100644 --- a/src/main/runtime/overlay-visibility-runtime-main-deps.ts +++ b/src/main/runtime/overlay-visibility-runtime-main-deps.ts @@ -14,8 +14,7 @@ export function createBuildOverlayVisibilityRuntimeMainDepsHandler( updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), - syncPrimaryOverlayWindowLayer: (layer: 'visible') => - deps.syncPrimaryOverlayWindowLayer(layer), + syncPrimaryOverlayWindowLayer: (layer: 'visible') => deps.syncPrimaryOverlayWindowLayer(layer), enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(), syncOverlayShortcuts: () => deps.syncOverlayShortcuts(), isMacOSPlatform: () => deps.isMacOSPlatform(), diff --git a/src/main/runtime/overlay-window-layout-main-deps.test.ts b/src/main/runtime/overlay-window-layout-main-deps.test.ts index 3b38dac..967854b 100644 --- a/src/main/runtime/overlay-window-layout-main-deps.test.ts +++ b/src/main/runtime/overlay-window-layout-main-deps.test.ts @@ -34,10 +34,5 @@ test('overlay window layout main deps builders map callbacks', () => { assert.deepEqual(order.getMainWindow(), { kind: 'main' }); order.ensureOverlayWindowLevel({}); - assert.deepEqual(calls, [ - 'visible', - 'ensure', - 'order', - 'order-level', - ]); + assert.deepEqual(calls, ['visible', 'ensure', 'order', 'order-level']); }); diff --git a/src/main/runtime/overlay-window-layout-main-deps.ts b/src/main/runtime/overlay-window-layout-main-deps.ts index fba3348..d544021 100644 --- a/src/main/runtime/overlay-window-layout-main-deps.ts +++ b/src/main/runtime/overlay-window-layout-main-deps.ts @@ -4,7 +4,9 @@ import type { createUpdateVisibleOverlayBoundsHandler, } from './overlay-window-layout'; -type UpdateVisibleOverlayBoundsMainDeps = Parameters[0]; +type UpdateVisibleOverlayBoundsMainDeps = Parameters< + typeof createUpdateVisibleOverlayBoundsHandler +>[0]; type EnsureOverlayWindowLevelMainDeps = Parameters[0]; type EnforceOverlayLayerOrderMainDeps = Parameters[0]; diff --git a/src/main/runtime/runtime-bootstrap-main-deps.test.ts b/src/main/runtime/runtime-bootstrap-main-deps.test.ts index d3fdca2..8b4aa7c 100644 --- a/src/main/runtime/runtime-bootstrap-main-deps.test.ts +++ b/src/main/runtime/runtime-bootstrap-main-deps.test.ts @@ -41,11 +41,11 @@ test('immersion media runtime main deps builder maps callbacks', async () => { test('anilist state runtime main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildAnilistStateRuntimeMainDepsHandler({ - getClientSecretState: () => ({ status: 'resolved' } as never), + getClientSecretState: () => ({ status: 'resolved' }) as never, setClientSecretState: () => calls.push('set-client'), - getRetryQueueState: () => ({ pending: 1 } as never), + getRetryQueueState: () => ({ pending: 1 }) as never, setRetryQueueState: () => calls.push('set-queue'), - getUpdateQueueSnapshot: () => ({ pending: 2 } as never), + getUpdateQueueSnapshot: () => ({ pending: 2 }) as never, clearStoredToken: () => calls.push('clear-stored'), clearCachedAccessToken: () => calls.push('clear-cached'), })(); @@ -62,7 +62,7 @@ test('anilist state runtime main deps builder maps callbacks', () => { test('config derived runtime main deps builder maps callbacks', () => { const deps = createBuildConfigDerivedRuntimeMainDepsHandler({ - getResolvedConfig: () => ({ jimaku: {} } as never), + getResolvedConfig: () => ({ jimaku: {} }) as never, getRuntimeOptionsManager: () => null, defaultJimakuLanguagePreference: 'ja', defaultJimakuMaxEntryResults: 20, @@ -80,7 +80,7 @@ test('main subsync runtime main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildMainSubsyncRuntimeMainDepsHandler({ getMpvClient: () => ({ connected: true }) as never, - getResolvedConfig: () => ({ subsync: {} } as never), + getResolvedConfig: () => ({ subsync: {} }) as never, getSubsyncInProgress: () => true, setSubsyncInProgress: () => calls.push('set-progress'), showMpvOsd: (text) => calls.push(`osd:${text}`), diff --git a/src/main/runtime/secondary-sub-mode-main-deps.test.ts b/src/main/runtime/secondary-sub-mode-main-deps.test.ts index 4858580..e68d4e3 100644 --- a/src/main/runtime/secondary-sub-mode-main-deps.test.ts +++ b/src/main/runtime/secondary-sub-mode-main-deps.test.ts @@ -18,7 +18,8 @@ test('cycle secondary sub mode main deps builder maps state and broadcasts with lastToggleAt = timestampMs; calls.push(`set-ts:${timestampMs}`); }, - broadcastToOverlayWindows: (channel, nextMode) => calls.push(`broadcast:${channel}:${nextMode}`), + broadcastToOverlayWindows: (channel, nextMode) => + calls.push(`broadcast:${channel}:${nextMode}`), showMpvOsd: (text) => calls.push(`osd:${text}`), })(); diff --git a/src/main/runtime/secondary-sub-mode-runtime-handler.test.ts b/src/main/runtime/secondary-sub-mode-runtime-handler.test.ts index a3c25d8..9da3515 100644 --- a/src/main/runtime/secondary-sub-mode-runtime-handler.test.ts +++ b/src/main/runtime/secondary-sub-mode-runtime-handler.test.ts @@ -21,9 +21,5 @@ test('secondary sub mode runtime handler composes deps for runtime call', () => }); handleCycleSecondarySubMode(); - assert.deepEqual(calls, [ - 'set-mode', - 'broadcast:secondary-subtitle:mode:romaji', - 'osd:romaji', - ]); + assert.deepEqual(calls, ['set-mode', 'broadcast:secondary-subtitle:mode:romaji', 'osd:romaji']); }); diff --git a/src/main/runtime/secondary-sub-mode-runtime-handler.ts b/src/main/runtime/secondary-sub-mode-runtime-handler.ts index 201e8ec..688d11c 100644 --- a/src/main/runtime/secondary-sub-mode-runtime-handler.ts +++ b/src/main/runtime/secondary-sub-mode-runtime-handler.ts @@ -11,7 +11,8 @@ export function createCycleSecondarySubModeRuntimeHandler(deps: { cycleSecondarySubModeMainDeps: CycleSecondarySubModeMainDeps; cycleSecondarySubMode: (deps: CycleSecondarySubModeDeps) => void; }) { - const buildCycleSecondarySubModeMainDepsHandler = - createBuildCycleSecondarySubModeMainDepsHandler(deps.cycleSecondarySubModeMainDeps); + const buildCycleSecondarySubModeMainDepsHandler = createBuildCycleSecondarySubModeMainDepsHandler( + deps.cycleSecondarySubModeMainDeps, + ); return () => deps.cycleSecondarySubMode(buildCycleSecondarySubModeMainDepsHandler()); } diff --git a/src/main/runtime/startup-config.test.ts b/src/main/runtime/startup-config.test.ts index a6fa17b..87a60a9 100644 --- a/src/main/runtime/startup-config.test.ts +++ b/src/main/runtime/startup-config.test.ts @@ -44,7 +44,9 @@ test('createReloadConfigHandler runs success flow with warnings', async () => { assert.ok(calls.some((entry) => entry.includes('1. ankiConnect.pollingRate: must be >= 50'))); assert.ok( calls.some((entry) => - entry.includes('dialog:SubMiner config validation warning:SubMiner detected config validation issues.'), + entry.includes( + 'dialog:SubMiner config validation warning:SubMiner detected config validation issues.', + ), ), ); assert.ok(calls.some((entry) => entry.includes('actual=10 fallback=250'))); diff --git a/src/main/runtime/startup-runtime-handlers.ts b/src/main/runtime/startup-runtime-handlers.ts index 7edf96f..69fcc8b 100644 --- a/src/main/runtime/startup-runtime-handlers.ts +++ b/src/main/runtime/startup-runtime-handlers.ts @@ -28,8 +28,9 @@ export function createStartupRuntimeHandlers< runStartupBootstrapRuntime: (deps: TStartupBootstrapRuntimeDeps) => TStartupState; applyStartupState: (startupState: TStartupState) => void; }) { - const appLifecycleRuntimeRunnerMainDeps = - createBuildAppLifecycleRuntimeRunnerMainDepsHandler(deps.appLifecycleRuntimeRunnerMainDeps)(); + const appLifecycleRuntimeRunnerMainDeps = createBuildAppLifecycleRuntimeRunnerMainDepsHandler( + deps.appLifecycleRuntimeRunnerMainDeps, + )(); const appLifecycleRuntimeRunner = deps.createAppLifecycleRuntimeRunner( appLifecycleRuntimeRunnerMainDeps, ); diff --git a/src/main/runtime/startup-warmups-main-deps.ts b/src/main/runtime/startup-warmups-main-deps.ts index 45cf122..675c775 100644 --- a/src/main/runtime/startup-warmups-main-deps.ts +++ b/src/main/runtime/startup-warmups-main-deps.ts @@ -3,7 +3,9 @@ import type { createStartBackgroundWarmupsHandler, } from './startup-warmups'; -type LaunchBackgroundWarmupTaskMainDeps = Parameters[0]; +type LaunchBackgroundWarmupTaskMainDeps = Parameters< + typeof createLaunchBackgroundWarmupTaskHandler +>[0]; type StartBackgroundWarmupsMainDeps = Parameters[0]; export function createBuildLaunchBackgroundWarmupTaskMainDepsHandler( @@ -16,7 +18,9 @@ export function createBuildLaunchBackgroundWarmupTaskMainDepsHandler( }); } -export function createBuildStartBackgroundWarmupsMainDepsHandler(deps: StartBackgroundWarmupsMainDeps) { +export function createBuildStartBackgroundWarmupsMainDepsHandler( + deps: StartBackgroundWarmupsMainDeps, +) { return (): StartBackgroundWarmupsMainDeps => ({ getStarted: () => deps.getStarted(), setStarted: (started: boolean) => deps.setStarted(started), diff --git a/src/main/runtime/subsync-runtime.ts b/src/main/runtime/subsync-runtime.ts index b0dcdb4..2019438 100644 --- a/src/main/runtime/subsync-runtime.ts +++ b/src/main/runtime/subsync-runtime.ts @@ -1,6 +1,14 @@ import type { MpvIpcClient } from '../../core/services'; -import { runSubsyncManualFromIpcRuntime, triggerSubsyncFromConfigRuntime } from '../../core/services'; -import type { SubsyncResult, SubsyncManualPayload, SubsyncManualRunRequest, ResolvedConfig } from '../../types'; +import { + runSubsyncManualFromIpcRuntime, + triggerSubsyncFromConfigRuntime, +} from '../../core/services'; +import type { + SubsyncResult, + SubsyncManualPayload, + SubsyncManualRunRequest, + ResolvedConfig, +} from '../../types'; import { getSubsyncConfig } from '../../subsync/utils'; import { createSubsyncRuntimeServiceInputFromState } from '../subsync-runtime'; diff --git a/src/main/runtime/subtitle-position-main-deps.test.ts b/src/main/runtime/subtitle-position-main-deps.test.ts index e2e2ee6..e11274f 100644 --- a/src/main/runtime/subtitle-position-main-deps.test.ts +++ b/src/main/runtime/subtitle-position-main-deps.test.ts @@ -8,7 +8,7 @@ import { test('load subtitle position main deps builder maps callbacks', () => { const calls: string[] = []; const deps = createBuildLoadSubtitlePositionMainDepsHandler({ - loadSubtitlePositionCore: () => ({ x: 1, y: 2 } as never), + loadSubtitlePositionCore: () => ({ x: 1, y: 2 }) as never, setSubtitlePosition: () => calls.push('set'), })(); diff --git a/src/main/runtime/subtitle-tokenization-main-deps.test.ts b/src/main/runtime/subtitle-tokenization-main-deps.test.ts index aa1654e..14c3a28 100644 --- a/src/main/runtime/subtitle-tokenization-main-deps.test.ts +++ b/src/main/runtime/subtitle-tokenization-main-deps.test.ts @@ -122,7 +122,10 @@ test('dictionary prewarm shows OSD spinner while loading and completion when loa } const tickCallback: () => void = tick; tickCallback(); - assert.deepEqual(osdMessages, ['Loading subtitle annotations |', 'Loading subtitle annotations /']); + assert.deepEqual(osdMessages, [ + 'Loading subtitle annotations |', + 'Loading subtitle annotations /', + ]); jlptDeferred.resolve(); freqDeferred.resolve(); diff --git a/src/main/runtime/subtitle-tokenization-main-deps.ts b/src/main/runtime/subtitle-tokenization-main-deps.ts index d047b42..d7699da 100644 --- a/src/main/runtime/subtitle-tokenization-main-deps.ts +++ b/src/main/runtime/subtitle-tokenization-main-deps.ts @@ -106,7 +106,9 @@ export function createPrewarmSubtitleDictionariesMainHandler(deps: { if (!showMpvOsd) { return; } - showMpvOsd(`Loading subtitle annotations ${spinnerFrames[loadingOsdFrame % spinnerFrames.length]}`); + showMpvOsd( + `Loading subtitle annotations ${spinnerFrames[loadingOsdFrame % spinnerFrames.length]}`, + ); loadingOsdFrame += 1; }, 180); return true; @@ -139,7 +141,10 @@ export function createPrewarmSubtitleDictionariesMainHandler(deps: { if (!prewarmPromise) { prewarmPromise = (async () => { try { - await Promise.all([deps.ensureJlptDictionaryLookup(), deps.ensureFrequencyDictionaryLookup()]); + await Promise.all([ + deps.ensureJlptDictionaryLookup(), + deps.ensureFrequencyDictionaryLookup(), + ]); prewarmed = true; } finally { prewarmPromise = null; diff --git a/src/main/runtime/tray-lifecycle.test.ts b/src/main/runtime/tray-lifecycle.test.ts index 551e69a..1d6bbd9 100644 --- a/src/main/runtime/tray-lifecycle.test.ts +++ b/src/main/runtime/tray-lifecycle.test.ts @@ -58,7 +58,11 @@ test('ensure tray creates new tray and binds click handler', () => { createImageFromPath: () => ({ isEmpty: () => false, - resize: (options: { width: number; height: number; quality?: 'best' | 'better' | 'good' }) => { + resize: (options: { + width: number; + height: number; + quality?: 'best' | 'better' | 'good'; + }) => { calls.push(`resize:${options.width}x${options.height}`); return { isEmpty: () => false, diff --git a/src/main/runtime/tray-lifecycle.ts b/src/main/runtime/tray-lifecycle.ts index fc78295..e9b0b60 100644 --- a/src/main/runtime/tray-lifecycle.ts +++ b/src/main/runtime/tray-lifecycle.ts @@ -1,6 +1,10 @@ type TrayIconLike = { isEmpty: () => boolean; - resize: (options: { width: number; height: number; quality?: 'best' | 'better' | 'good' }) => TrayIconLike; + resize: (options: { + width: number; + height: number; + quality?: 'best' | 'better' | 'good'; + }) => TrayIconLike; setTemplateImage: (enabled: boolean) => void; }; diff --git a/src/main/runtime/tray-runtime-handlers.test.ts b/src/main/runtime/tray-runtime-handlers.test.ts index 75374b5..fcfdb31 100644 --- a/src/main/runtime/tray-runtime-handlers.test.ts +++ b/src/main/runtime/tray-runtime-handlers.test.ts @@ -42,14 +42,14 @@ test('tray runtime handlers compose resolve/menu/ensure/destroy handlers', () => isEmpty: () => false, resize: () => ({ isEmpty: () => false, - resize: () => ({} as never), + resize: () => ({}) as never, setTemplateImage: () => {}, }), setTemplateImage: () => {}, }), createEmptyImage: () => ({ isEmpty: () => true, - resize: () => ({} as never), + resize: () => ({}) as never, setTemplateImage: () => {}, }), createTray: () => ({ diff --git a/src/main/runtime/yomitan-extension-loader-main-deps.ts b/src/main/runtime/yomitan-extension-loader-main-deps.ts index cb28a29..7acb73f 100644 --- a/src/main/runtime/yomitan-extension-loader-main-deps.ts +++ b/src/main/runtime/yomitan-extension-loader-main-deps.ts @@ -8,9 +8,7 @@ type EnsureYomitanExtensionLoadedMainDeps = Parameters< typeof createEnsureYomitanExtensionLoadedHandler >[0]; -export function createBuildLoadYomitanExtensionMainDepsHandler( - deps: LoadYomitanExtensionMainDeps, -) { +export function createBuildLoadYomitanExtensionMainDepsHandler(deps: LoadYomitanExtensionMainDeps) { return (): LoadYomitanExtensionMainDeps => ({ loadYomitanExtensionCore: (options) => deps.loadYomitanExtensionCore(options), userDataPath: deps.userDataPath, diff --git a/src/main/runtime/yomitan-extension-runtime.ts b/src/main/runtime/yomitan-extension-runtime.ts index 51830d0..7ecf9ad 100644 --- a/src/main/runtime/yomitan-extension-runtime.ts +++ b/src/main/runtime/yomitan-extension-runtime.ts @@ -1,4 +1,7 @@ -import { createEnsureYomitanExtensionLoadedHandler, createLoadYomitanExtensionHandler } from './yomitan-extension-loader'; +import { + createEnsureYomitanExtensionLoadedHandler, + createLoadYomitanExtensionHandler, +} from './yomitan-extension-loader'; import { createBuildEnsureYomitanExtensionLoadedMainDepsHandler, createBuildLoadYomitanExtensionMainDepsHandler, diff --git a/src/main/runtime/yomitan-settings-runtime.ts b/src/main/runtime/yomitan-settings-runtime.ts index 4671678..99b610b 100644 --- a/src/main/runtime/yomitan-settings-runtime.ts +++ b/src/main/runtime/yomitan-settings-runtime.ts @@ -1,7 +1,9 @@ import { createBuildOpenYomitanSettingsMainDepsHandler } from './app-runtime-main-deps'; import { createOpenYomitanSettingsHandler } from './yomitan-settings-opener'; -type OpenYomitanSettingsMainDeps = Parameters[0]; +type OpenYomitanSettingsMainDeps = Parameters< + typeof createBuildOpenYomitanSettingsMainDepsHandler +>[0]; export function createYomitanSettingsRuntime(deps: OpenYomitanSettingsMainDeps) { const openYomitanSettingsMainDeps = createBuildOpenYomitanSettingsMainDepsHandler(deps)(); diff --git a/src/preload.ts b/src/preload.ts index 28c27eb..47a1e47 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -59,9 +59,7 @@ const overlayLayer = type EmptyListener = () => void; type PayloadedListener = (payload: T) => void; -function createQueuedIpcListener( - channel: string, -): (listener: EmptyListener) => void { +function createQueuedIpcListener(channel: string): (listener: EmptyListener) => void { let count = 0; const listeners: EmptyListener[] = []; @@ -124,10 +122,11 @@ const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload payload as SubsyncManualPayload, ); -const onKikuFieldGroupingRequestEvent = createQueuedIpcListenerWithPayload( - IPC_CHANNELS.event.kikuFieldGroupingRequest, - (payload) => payload as KikuFieldGroupingRequestData, -); +const onKikuFieldGroupingRequestEvent = + createQueuedIpcListenerWithPayload( + IPC_CHANNELS.event.kikuFieldGroupingRequest, + (payload) => payload as KikuFieldGroupingRequestData, + ); const electronAPI: ElectronAPI = { getOverlayLayer: () => overlayLayer, diff --git a/src/renderer/error-recovery.test.ts b/src/renderer/error-recovery.test.ts index a94821c..ffbbdd3 100644 --- a/src/renderer/error-recovery.test.ts +++ b/src/renderer/error-recovery.test.ts @@ -7,6 +7,7 @@ import { hasYomitanPopupIframe, isYomitanPopupIframe, } from './yomitan-popup.js'; +import { scrollActiveRuntimeOptionIntoView } from './modals/runtime-options.js'; import { resolvePlatformInfo } from './utils/platform.js'; test('handleError logs context and recovers overlay state', () => { @@ -281,3 +282,32 @@ test('hasYomitanPopupIframe queries for modern + legacy selector', () => { assert.equal(hasYomitanPopupIframe(root), true); assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR); }); + +test('scrollActiveRuntimeOptionIntoView scrolls active runtime option with nearest block', () => { + const calls: Array<{ block?: ScrollLogicalPosition }> = []; + const activeItem = { + scrollIntoView: (options?: ScrollIntoViewOptions) => { + calls.push({ block: options?.block }); + }, + }; + + const list = { + querySelector: (selector: string) => { + assert.equal(selector, '.runtime-options-item.active'); + return activeItem as unknown as Element; + }, + }; + + scrollActiveRuntimeOptionIntoView(list); + assert.deepEqual(calls, [{ block: 'nearest' }]); +}); + +test('scrollActiveRuntimeOptionIntoView no-ops without active option', () => { + const list = { + querySelector: () => null, + }; + + assert.doesNotThrow(() => { + scrollActiveRuntimeOptionIntoView(list); + }); +}); diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 7598aa0..2749eff 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -170,13 +170,7 @@ export function createKeyboardHandlers( return; } - if ( - (e.ctrlKey || e.metaKey) && - !e.altKey && - !e.shiftKey && - e.code === 'KeyA' && - !e.repeat - ) { + if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && e.code === 'KeyA' && !e.repeat) { e.preventDefault(); options.appendClipboardVideoToQueue(); return; diff --git a/src/renderer/handlers/mouse.ts b/src/renderer/handlers/mouse.ts index 5e94dc9..5459a6a 100644 --- a/src/renderer/handlers/mouse.ts +++ b/src/renderer/handlers/mouse.ts @@ -35,10 +35,7 @@ export function createMouseHandlers( } yomitanPopupVisible = false; - if ( - !ctx.state.isOverSubtitle && - !options.modalStateReader.isAnyModalOpen() - ) { + if (!ctx.state.isOverSubtitle && !options.modalStateReader.isAnyModalOpen()) { ctx.dom.overlay.classList.remove('interactive'); if (ctx.platform.shouldToggleMouseIgnore) { window.electronAPI.setIgnoreMouseEvents(true, { forward: true }); diff --git a/src/renderer/modals/runtime-options.ts b/src/renderer/modals/runtime-options.ts index 7162797..8b9cfdb 100644 --- a/src/renderer/modals/runtime-options.ts +++ b/src/renderer/modals/runtime-options.ts @@ -1,6 +1,20 @@ import type { RuntimeOptionApplyResult, RuntimeOptionState, RuntimeOptionValue } from '../../types'; import type { ModalStateReader, RendererContext } from '../context'; +type RuntimeOptionsListLike = Pick; + +export function scrollActiveRuntimeOptionIntoView(list: RuntimeOptionsListLike): void { + const active = list.querySelector('.runtime-options-item.active'); + if (!active) return; + + const maybeScrollable = active as unknown as { + scrollIntoView?: (options?: ScrollIntoViewOptions) => void; + }; + if (typeof maybeScrollable.scrollIntoView !== 'function') return; + + maybeScrollable.scrollIntoView({ block: 'nearest' }); +} + export function createRuntimeOptionsModal( ctx: RendererContext, options: { @@ -82,6 +96,8 @@ export function createRuntimeOptionsModal( ctx.dom.runtimeOptionsList.appendChild(li); }); + + scrollActiveRuntimeOptionIntoView(ctx.dom.runtimeOptionsList); } function updateRuntimeOptions(optionsList: RuntimeOptionState[]): void { diff --git a/src/renderer/positioning/controller.ts b/src/renderer/positioning/controller.ts index 7a0f844..ae4ef1c 100644 --- a/src/renderer/positioning/controller.ts +++ b/src/renderer/positioning/controller.ts @@ -1,11 +1,9 @@ import type { RendererContext } from '../context'; import { createInMemorySubtitlePositionController, - type SubtitlePositionController + type SubtitlePositionController, } from './position-state.js'; -export function createPositioningController( - ctx: RendererContext, -): SubtitlePositionController { +export function createPositioningController(ctx: RendererContext): SubtitlePositionController { return createInMemorySubtitlePositionController(ctx); } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index da1682a..98f0a33 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -343,7 +343,10 @@ async function init(): Promise { subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle()); - positioning.applyStoredSubtitlePosition(await window.electronAPI.getSubtitlePosition(), 'startup'); + positioning.applyStoredSubtitlePosition( + await window.electronAPI.getSubtitlePosition(), + 'startup', + ); measurementReporter.schedule(); if (ctx.platform.shouldToggleMouseIgnore) { diff --git a/src/renderer/style.css b/src/renderer/style.css index d6a288c..9d4f969 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -498,7 +498,8 @@ body.settings-modal-open #subtitleContainer { color: var(--subtitle-frequency-band-5-color, #8aadf4); } -#subtitleRoot .word:not(.word-known):not(.word-n-plus-one):not(.word-frequency-single):not( +#subtitleRoot + .word:not(.word-known):not(.word-n-plus-one):not(.word-frequency-single):not( .word-frequency-band-1 ):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not( .word-frequency-band-5 diff --git a/src/renderer/subtitle-render.test.ts b/src/renderer/subtitle-render.test.ts index a8f9009..2177248 100644 --- a/src/renderer/subtitle-render.test.ts +++ b/src/renderer/subtitle-render.test.ts @@ -276,7 +276,10 @@ test('alignTokensToSourceText treats whitespace-only token surfaces as plain tex createToken({ surface: '体が耐えきれず死に至るが…' }), ]; - const segments = alignTokensToSourceText(tokens, '常人が使えば その圧倒的な力に\n体が耐えきれず死に至るが…'); + const segments = alignTokensToSourceText( + tokens, + '常人が使えば その圧倒的な力に\n体が耐えきれず死に至るが…', + ); assert.deepEqual( segments.map((segment) => (segment.kind === 'text' ? `text:${segment.text}` : 'token')), ['token', 'text: ', 'token', 'text:\n', 'token'], @@ -300,10 +303,7 @@ test('alignTokensToSourceText avoids duplicate tail when later token surface doe }); test('buildSubtitleTokenHoverRanges tracks token offsets across text separators', () => { - const tokens = [ - createToken({ surface: 'キリキリと' }), - createToken({ surface: 'かかってこい' }), - ]; + const tokens = [createToken({ surface: 'キリキリと' }), createToken({ surface: 'かかってこい' })]; const ranges = buildSubtitleTokenHoverRanges(tokens, 'キリキリと\nかかってこい'); assert.deepEqual(ranges, [ @@ -318,7 +318,10 @@ test('buildSubtitleTokenHoverRanges ignores unmatched token surfaces', () => { createToken({ surface: '教団の主力は1人もいない' }), ]; - const ranges = buildSubtitleTokenHoverRanges(tokens, '君たちが潰した拠点に\n教団の主力は1人もいない'); + const ranges = buildSubtitleTokenHoverRanges( + tokens, + '君たちが潰した拠点に\n教団の主力は1人もいない', + ); assert.deepEqual(ranges, [{ start: 0, end: 10, tokenIndex: 0 }]); }); @@ -378,10 +381,7 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { assert.match(visibleMacBlock, /--visible-sub-line-gap:\s*0\.54em;/); const subtitleRootBlock = extractClassBlock(cssText, '#subtitleRoot'); - assert.match( - subtitleRootBlock, - /--subtitle-hover-token-color:\s*#f4dbd6;/, - ); + assert.match(subtitleRootBlock, /--subtitle-hover-token-color:\s*#f4dbd6;/); assert.match( subtitleRootBlock, /--subtitle-hover-token-background-color:\s*rgba\(54,\s*58,\s*79,\s*0\.84\);/, @@ -394,7 +394,10 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { const wordBlock = extractClassBlock(cssText, '#subtitleRoot .word'); assert.match(wordBlock, /-webkit-text-fill-color:\s*currentColor\s*!important;/); - const frequencyTooltipBaseBlock = extractClassBlock(cssText, '#subtitleRoot .word[data-frequency-rank]::before'); + const frequencyTooltipBaseBlock = extractClassBlock( + cssText, + '#subtitleRoot .word[data-frequency-rank]::before', + ); assert.match(frequencyTooltipBaseBlock, /content:\s*attr\(data-frequency-rank\);/); assert.match(frequencyTooltipBaseBlock, /opacity:\s*0;/); assert.match(frequencyTooltipBaseBlock, /pointer-events:\s*none;/); @@ -405,7 +408,10 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { ); assert.match(frequencyTooltipHoverBlock, /opacity:\s*1;/); - const jlptTooltipBaseBlock = extractClassBlock(cssText, '#subtitleRoot .word[data-jlpt-level]::after'); + const jlptTooltipBaseBlock = extractClassBlock( + cssText, + '#subtitleRoot .word[data-jlpt-level]::after', + ); assert.match(jlptTooltipBaseBlock, /content:\s*attr\(data-jlpt-level\);/); assert.match(jlptTooltipBaseBlock, /bottom:\s*-\s*0\.42em;/); assert.match(jlptTooltipBaseBlock, /opacity:\s*0;/); @@ -430,9 +436,15 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { assert.match(coloredWordHoverBlock, /border-radius:\s*3px;/); assert.match(coloredWordHoverBlock, /font-weight:\s*800;/); assert.doesNotMatch(coloredWordHoverBlock, /color:\s*var\(--subtitle-hover-token-color/); - assert.doesNotMatch(coloredWordHoverBlock, /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color/); + assert.doesNotMatch( + coloredWordHoverBlock, + /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color/, + ); - const coloredWordSelectionBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known::selection'); + const coloredWordSelectionBlock = extractClassBlock( + cssText, + '#subtitleRoot .word.word-known::selection', + ); assert.match( coloredWordSelectionBlock, /color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/, @@ -442,7 +454,10 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { /-webkit-text-fill-color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/, ); - const coloredCharHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known .c:hover'); + const coloredCharHoverBlock = extractClassBlock( + cssText, + '#subtitleRoot .word.word-known .c:hover', + ); assert.match(coloredCharHoverBlock, /background:\s*transparent;/); assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/); @@ -460,7 +475,10 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => { selectionBlock, /background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/, ); - assert.match(selectionBlock, /color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/); + assert.match( + selectionBlock, + /color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/, + ); assert.match( selectionBlock, /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/, diff --git a/src/renderer/subtitle-render.ts b/src/renderer/subtitle-render.ts index 2438e05..45bad5d 100644 --- a/src/renderer/subtitle-render.ts +++ b/src/renderer/subtitle-render.ts @@ -49,7 +49,12 @@ function sanitizeHexColor(value: unknown, fallback: string): string { export function sanitizeSubtitleHoverTokenColor(value: unknown): string { const sanitized = sanitizeHexColor(value, '#f4dbd6'); const normalized = sanitized.replace(/^#/, '').toLowerCase(); - if (normalized === '000' || normalized === '0000' || normalized === '000000' || normalized === '00000000') { + if ( + normalized === '000' || + normalized === '0000' || + normalized === '000000' || + normalized === '00000000' + ) { return '#f4dbd6'; } return sanitized; @@ -257,7 +262,10 @@ function renderWithTokens( span.dataset.tokenIndex = String(segment.tokenIndex); if (token.reading) span.dataset.reading = token.reading; if (token.headword) span.dataset.headword = token.headword; - const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedFrequencyRenderSettings); + const frequencyRankLabel = getFrequencyRankLabelForToken( + token, + resolvedFrequencyRenderSettings, + ); if (frequencyRankLabel) { span.dataset.frequencyRank = frequencyRankLabel; } @@ -293,7 +301,10 @@ function renderWithTokens( span.dataset.tokenIndex = String(index); if (token.reading) span.dataset.reading = token.reading; if (token.headword) span.dataset.headword = token.headword; - const frequencyRankLabel = getFrequencyRankLabelForToken(token, resolvedFrequencyRenderSettings); + const frequencyRankLabel = getFrequencyRankLabelForToken( + token, + resolvedFrequencyRenderSettings, + ); if (frequencyRankLabel) { span.dataset.frequencyRank = frequencyRankLabel; } @@ -460,7 +471,7 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void } export function createSubtitleRenderer(ctx: RendererContext) { -function renderSubtitle(data: SubtitleData | string): void { + function renderSubtitle(data: SubtitleData | string): void { ctx.dom.subtitleRoot.innerHTML = ''; let text: string; @@ -551,11 +562,7 @@ function renderSubtitle(data: SubtitleData | string): void { if (!style) return; const styleDeclarations = style as Record; - applyInlineStyleDeclarations( - ctx.dom.subtitleRoot, - styleDeclarations, - CONTAINER_STYLE_KEYS, - ); + applyInlineStyleDeclarations(ctx.dom.subtitleRoot, styleDeclarations, CONTAINER_STYLE_KEYS); applyInlineStyleDeclarations( ctx.dom.subtitleContainer, pickInlineStyleDeclarations(styleDeclarations, CONTAINER_STYLE_KEYS), diff --git a/src/runtime-options.ts b/src/runtime-options.ts index 0b1ce70..abff64e 100644 --- a/src/runtime-options.ts +++ b/src/runtime-options.ts @@ -22,6 +22,7 @@ import { RuntimeOptionId, RuntimeOptionState, RuntimeOptionValue, + SubtitleStyleConfig, } from './types'; import { RUNTIME_OPTION_REGISTRY, RuntimeOptionRegistryEntry } from './config'; @@ -78,6 +79,7 @@ function isAllowedValue( export class RuntimeOptionsManager { private readonly getAnkiConfig: () => AnkiConnectConfig; + private readonly getSubtitleStyleConfig?: () => SubtitleStyleConfig; private readonly applyAnkiPatch: (patch: Partial) => void; private readonly onOptionsChanged: (options: RuntimeOptionState[]) => void; private runtimeOverrides: RuntimeOverrides = {}; @@ -88,9 +90,11 @@ export class RuntimeOptionsManager { callbacks: { applyAnkiPatch: (patch: Partial) => void; onOptionsChanged: (options: RuntimeOptionState[]) => void; + getSubtitleStyleConfig?: () => SubtitleStyleConfig; }, ) { this.getAnkiConfig = getAnkiConfig; + this.getSubtitleStyleConfig = callbacks.getSubtitleStyleConfig; this.applyAnkiPatch = callbacks.applyAnkiPatch; this.onOptionsChanged = callbacks.onOptionsChanged; for (const definition of RUNTIME_OPTION_REGISTRY) { @@ -104,6 +108,7 @@ export class RuntimeOptionsManager { const source = { ankiConnect: this.getAnkiConfig(), + subtitleStyle: this.getSubtitleStyleConfig?.(), } as Record; const raw = getPathValue(source, definition.path); @@ -152,8 +157,10 @@ export class RuntimeOptionsManager { setPathValue(next, definition.path, value); this.runtimeOverrides = next; - const ankiPatch = definition.toAnkiPatch(value); - this.applyAnkiPatch(ankiPatch); + if (definition.scope === 'ankiConnect') { + const ankiPatch = definition.toAnkiPatch(value); + this.applyAnkiPatch(ankiPatch); + } const option = this.listOptions().find((item) => item.id === id); if (!option) { @@ -198,6 +205,7 @@ export class RuntimeOptionsManager { for (const definition of RUNTIME_OPTION_REGISTRY) { const override = getPathValue(this.runtimeOverrides, definition.path); if (override === undefined) continue; + if (!definition.path.startsWith('ankiConnect.')) continue; const subPath = definition.path.replace(/^ankiConnect\./, ''); setPathValue(effective as unknown as Record, subPath, override); diff --git a/src/shared/ipc/validators.ts b/src/shared/ipc/validators.ts index babd923..70df3d1 100644 --- a/src/shared/ipc/validators.ts +++ b/src/shared/ipc/validators.ts @@ -13,6 +13,9 @@ import { OVERLAY_HOSTED_MODALS, type OverlayHostedModal } from './contracts'; const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [ 'anki.autoUpdateNewCards', + 'subtitle.annotation.nPlusOne', + 'subtitle.annotation.jlpt', + 'subtitle.annotation.frequency', 'anki.kikuFieldGrouping', 'anki.nPlusOneMatchMode', ]; diff --git a/src/token-merger.ts b/src/token-merger.ts index c30d986..f333b01 100644 --- a/src/token-merger.ts +++ b/src/token-merger.ts @@ -243,12 +243,8 @@ export function mergeTokens( } const SENTENCE_BOUNDARY_SURFACES = new Set(['。', '?', '!', '?', '!', '…', '\u2026']); -const N_PLUS_ONE_IGNORED_POS1 = new Set( - DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG.defaults, -); -const N_PLUS_ONE_IGNORED_POS2 = new Set( - DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG.defaults, -); +const N_PLUS_ONE_IGNORED_POS1 = new Set(DEFAULT_ANNOTATION_POS1_EXCLUSION_CONFIG.defaults); +const N_PLUS_ONE_IGNORED_POS2 = new Set(DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG.defaults); function normalizePos1Tag(pos1: string | undefined): string { return typeof pos1 === 'string' ? pos1.trim() : ''; @@ -258,10 +254,7 @@ function normalizePos2Tag(pos2: string | undefined): string { return typeof pos2 === 'string' ? pos2.trim() : ''; } -function isExcludedByTagSet( - normalizedTag: string, - exclusions: ReadonlySet, -): boolean { +function isExcludedByTagSet(normalizedTag: string, exclusions: ReadonlySet): boolean { if (!normalizedTag) { return false; } @@ -303,11 +296,13 @@ function isNPlusOneWordCountToken( return false; } - if (!hasPos1 && !hasPos2 && ( - token.partOfSpeech === PartOfSpeech.particle || - token.partOfSpeech === PartOfSpeech.bound_auxiliary || - token.partOfSpeech === PartOfSpeech.symbol - )) { + if ( + !hasPos1 && + !hasPos2 && + (token.partOfSpeech === PartOfSpeech.particle || + token.partOfSpeech === PartOfSpeech.bound_auxiliary || + token.partOfSpeech === PartOfSpeech.symbol) + ) { return false; } diff --git a/src/token-pos2-exclusions.ts b/src/token-pos2-exclusions.ts index a6eeef2..01b1439 100644 --- a/src/token-pos2-exclusions.ts +++ b/src/token-pos2-exclusions.ts @@ -1,7 +1,9 @@ import type { ResolvedTokenPos2ExclusionConfig, TokenPos2ExclusionConfig } from './types'; import { normalizePos1ExclusionList } from './token-pos1-exclusions'; -export const DEFAULT_ANNOTATION_POS2_EXCLUSION_DEFAULTS = Object.freeze(['非自立']) as readonly string[]; +export const DEFAULT_ANNOTATION_POS2_EXCLUSION_DEFAULTS = Object.freeze([ + '非自立', +]) as readonly string[]; export const DEFAULT_ANNOTATION_POS2_EXCLUSION_CONFIG: ResolvedTokenPos2ExclusionConfig = { defaults: [...DEFAULT_ANNOTATION_POS2_EXCLUSION_DEFAULTS], diff --git a/src/types.ts b/src/types.ts index 8830a27..1505d14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,10 +167,13 @@ export interface KikuMergePreviewResponse { export type RuntimeOptionId = | 'anki.autoUpdateNewCards' + | 'subtitle.annotation.nPlusOne' + | 'subtitle.annotation.jlpt' + | 'subtitle.annotation.frequency' | 'anki.kikuFieldGrouping' | 'anki.nPlusOneMatchMode'; -export type RuntimeOptionScope = 'ankiConnect'; +export type RuntimeOptionScope = 'ankiConnect' | 'subtitle'; export type RuntimeOptionValueType = 'boolean' | 'enum';