From 166015897d0d347df6b33f2d5acb4541d31533a0 Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 20 May 2026 20:31:02 -0700 Subject: [PATCH] rename config window to settings and update CLI entry points - Replace `--config`/`subminer config` (no action) with `--settings`/`subminer settings` - `subminer config` now requires an explicit action (`path` or `show`) - `--settings` previously opened Yomitan; replaced by `--yomitan` - Linux tray update installs AppImage via electron-updater instead of manual flow - macOS update dialog activation and curl-fetch routing fixes - Delete stale compiled artifacts (main.js, app-updater.js) --- README.md | 4 +- app-updater.js | 193 - changes/config-settings-controls.md | 2 +- changes/config-settings-search.md | 2 +- changes/config-settings-window.md | 10 +- changes/fix-known-words-decks-row-overflow.md | 4 + .../fix-launcher-electron-menu-diagnostic.md | 2 +- changes/linux-tray-appimage-update.md | 5 + changes/macos-config-window-exit.md | 4 + changes/macos-update-dialog-activation.md | 4 + changes/macos-updater-curl-fetch.md | 4 + changes/rename-config-window-to-settings.md | 7 + changes/settings-modal-layout.md | 1 + changes/subtitle-css-live-settings.md | 2 +- docs-site/configuration.md | 2 +- docs-site/installation.md | 4 +- docs-site/launcher-script.md | 1 + docs-site/troubleshooting.md | 2 +- docs-site/usage.md | 8 +- launcher/commands/app-command.ts | 4 +- launcher/commands/playback-command.test.ts | 2 +- launcher/config/args-normalizer.test.ts | 48 +- launcher/config/args-normalizer.ts | 16 +- launcher/config/cli-parser-builder.ts | 18 +- launcher/main.test.ts | 16 +- launcher/mpv.test.ts | 2 +- launcher/parse-args.test.ts | 22 +- launcher/types.ts | 2 +- main.js | 4711 ----------------- src/cli/args.test.ts | 38 +- src/cli/args.ts | 20 +- src/cli/help.test.ts | 3 +- src/cli/help.ts | 4 +- src/config/settings/registry.test.ts | 65 +- src/config/settings/registry.ts | 49 +- src/core/services/app-lifecycle.test.ts | 21 +- src/core/services/app-lifecycle.ts | 5 +- src/core/services/cli-command.test.ts | 6 +- src/core/services/cli-command.ts | 4 +- src/core/services/config-hot-reload.test.ts | 2 + src/core/services/config-hot-reload.ts | 1 + src/core/services/startup-bootstrap.test.ts | 2 +- src/main.ts | 64 +- src/main/runtime/config-settings-ipc.test.ts | 2 +- src/main/runtime/config-settings-window.ts | 2 +- .../runtime/first-run-setup-service.test.ts | 6 +- src/main/runtime/first-run-setup-service.ts | 2 +- src/main/runtime/setup-window-factory.test.ts | 2 +- src/main/runtime/setup-window-factory.ts | 2 +- src/main/runtime/startup-mode-flags.test.ts | 4 +- src/main/runtime/startup-mode-flags.ts | 12 +- src/main/runtime/tray-runtime.ts | 2 +- src/main/runtime/update/app-updater.test.ts | 30 +- src/main/runtime/update/app-updater.ts | 22 +- .../runtime/update/update-dialogs.test.ts | 43 +- src/main/runtime/update/update-dialogs.ts | 14 +- src/settings/index.html | 8 +- src/settings/settings-anki-controls.ts | 11 +- src/settings/settings-controls.ts | 5 +- src/settings/settings-model.test.ts | 11 +- src/settings/settings-model.ts | 20 + src/settings/style.css | 25 +- update-service.js | 172 - 63 files changed, 500 insertions(+), 5281 deletions(-) delete mode 100644 app-updater.js create mode 100644 changes/fix-known-words-decks-row-overflow.md create mode 100644 changes/linux-tray-appimage-update.md create mode 100644 changes/macos-config-window-exit.md create mode 100644 changes/macos-update-dialog-activation.md create mode 100644 changes/macos-updater-curl-fetch.md create mode 100644 changes/rename-config-window-to-settings.md create mode 100644 changes/settings-modal-layout.md delete mode 100644 main.js delete mode 100644 update-service.js diff --git a/README.md b/README.md index b726691c..82f9083d 100644 --- a/README.md +++ b/README.md @@ -210,8 +210,8 @@ On **Windows**, just run `SubMiner.exe` — setup opens automatically on first l ```bash subminer video.mkv # play video with overlay subminer stats # open immersion dashboard -subminer config # open configuration window -subminer --config # open configuration window via flag +subminer settings # open settings window +subminer --settings # open settings window via flag ``` On **Windows**, use the **SubMiner mpv** shortcut created during setup — double-click it or drag a video file onto it. diff --git a/app-updater.js b/app-updater.js deleted file mode 100644 index 4709efa2..00000000 --- a/app-updater.js +++ /dev/null @@ -1,193 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.resolveMacAppBundlePath = resolveMacAppBundlePath; -exports.isMacApplicationsFolderBundle = isMacApplicationsFolderBundle; -exports.isKnownLinuxPackageManagedAppImage = isKnownLinuxPackageManagedAppImage; -exports.isNativeUpdaterSupported = isNativeUpdaterSupported; -exports.configureAutoUpdater = configureAutoUpdater; -exports.createElectronAppUpdater = createElectronAppUpdater; -const node_fs_1 = require("node:fs"); -const node_child_process_1 = require("node:child_process"); -const node_os_1 = __importDefault(require("node:os")); -const node_path_1 = __importDefault(require("node:path")); -const node_util_1 = require("node:util"); -const electron_updater_1 = require("electron-updater"); -const release_assets_1 = require("./release-assets"); -const updaterErrorListeners = new WeakMap(); -const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile); -function resolveMacAppBundlePath(execPath) { - const marker = '.app/Contents/MacOS/'; - const markerIndex = execPath.indexOf(marker); - if (markerIndex < 0) - return null; - return execPath.slice(0, markerIndex + '.app'.length); -} -async function readMacCodeSignature(appBundlePath) { - try { - const result = await execFileAsync('/usr/bin/codesign', ['-dv', '--verbose=4', appBundlePath], { - encoding: 'utf8', - }); - return `${result.stdout ?? ''}\n${result.stderr ?? ''}`; - } - catch { - return null; - } -} -function realpathOrOriginal(filePath) { - try { - return (0, node_fs_1.realpathSync)(filePath); - } - catch { - return filePath; - } -} -function isSameOrInsideDirectory(parentPath, candidatePath) { - const relative = node_path_1.default.relative(parentPath, candidatePath); - return (relative === '' || - (relative.length > 0 && !relative.startsWith('..') && !node_path_1.default.isAbsolute(relative))); -} -function isMacApplicationsFolderBundle(appBundlePath, homeDir = node_os_1.default.homedir()) { - const resolvedBundlePath = node_path_1.default.resolve(appBundlePath); - return (isSameOrInsideDirectory('/Applications', resolvedBundlePath) || - isSameOrInsideDirectory(node_path_1.default.join(homeDir, 'Applications'), resolvedBundlePath)); -} -function isKnownLinuxPackageManagedAppImage(appImagePath) { - return realpathOrOriginal(appImagePath) === '/opt/SubMiner/SubMiner.AppImage'; -} -async function isNativeUpdaterSupported(options) { - if (!options.isPackaged) { - options.log?.('Skipping native updater because this build is not packaged.'); - return false; - } - if (options.platform === 'linux') { - options.log?.('Skipping native Linux updater because Linux tray checks use GitHub release assets.'); - return false; - } - if (options.platform !== 'darwin') { - options.log?.('Skipping native updater because this platform uses GitHub metadata checks.'); - return false; - } - const appBundlePath = resolveMacAppBundlePath(options.execPath); - if (!appBundlePath) { - options.log?.('Skipping native macOS updater because the app bundle path could not be resolved.'); - return false; - } - if (!isMacApplicationsFolderBundle(appBundlePath, options.homeDir)) { - options.log?.('Skipping native macOS updater because the app is not installed in an Applications folder.'); - return false; - } - const signature = await (options.readCodeSignature ?? readMacCodeSignature)(appBundlePath); - if (!signature) { - options.log?.('Skipping native macOS updater because the app code signature could not be read.'); - return false; - } - if (/Signature=adhoc\b/.test(signature) || /TeamIdentifier=not set\b/.test(signature)) { - options.log?.('Skipping native macOS updater because this build is ad-hoc signed.'); - return false; - } - return true; -} -function configureAutoUpdater(updater, log = () => { }, channel = 'stable') { - updater.autoDownload = false; - // On macOS this avoids invoking Squirrel until the explicit restart/install step. - updater.autoInstallOnAppQuit = false; - updater.allowPrerelease = channel === 'prerelease'; - updater.allowDowngrade = false; - updater.logger = { - info: () => { }, - debug: () => { }, - warn: (message) => log(message), - error: (message) => log(message), - }; - const previousErrorListener = updaterErrorListeners.get(updater); - if (previousErrorListener) { - if (updater.off) { - updater.off('error', previousErrorListener); - } - else { - updater.removeListener?.('error', previousErrorListener); - } - } - if (updater.on) { - const errorListener = (error) => { - const message = error instanceof Error ? error.message : String(error); - log(`Updater error event: ${message}`); - }; - updater.on('error', errorListener); - updaterErrorListeners.set(updater, errorListener); - } - return updater; -} -function createElectronAppUpdater(options) { - const getChannel = options.getChannel ?? (() => 'stable'); - const updater = configureAutoUpdater(options.updater ?? electron_updater_1.autoUpdater, options.log, getChannel()); - if (options.configureHttpExecutor) { - // electron-updater has no public executor hook; keep the macOS cURL override localized. - updater.httpExecutor = options.configureHttpExecutor(); - } - if (options.disableDifferentialDownload !== undefined) { - updater.disableDifferentialDownload = options.disableDifferentialDownload; - } - let nativeUpdaterSupported = null; - async function getNativeUpdaterSupported() { - if (!options.isNativeUpdaterSupported) - return true; - if (nativeUpdaterSupported === null) { - nativeUpdaterSupported = Promise.resolve(options.isNativeUpdaterSupported()); - } - return nativeUpdaterSupported; - } - return { - async checkForUpdates(channel) { - if (!options.isPackaged) { - return { - available: false, - version: options.currentVersion, - canUpdate: false, - }; - } - if (!(await getNativeUpdaterSupported())) { - options.log('Skipping native app update check because native updater is unsupported.'); - return { - available: false, - version: options.currentVersion, - canUpdate: false, - }; - } - configureAutoUpdater(updater, options.log, channel ?? getChannel()); - const result = await updater.checkForUpdates(); - const version = result?.updateInfo?.version ?? options.currentVersion; - return { - available: (0, release_assets_1.compareSemverLike)(version, options.currentVersion) > 0, - version, - canUpdate: true, - }; - }, - async downloadUpdate() { - if (!options.isPackaged) { - options.log('Skipping app update download because this build is not packaged.'); - return; - } - if (!(await getNativeUpdaterSupported())) { - options.log('Skipping app update download because native updater is unsupported.'); - return; - } - await updater.downloadUpdate(); - }, - async quitAndInstall() { - if (!options.isPackaged) { - options.log('Skipping app update install because this build is not packaged.'); - return; - } - if (!(await getNativeUpdaterSupported())) { - options.log('Skipping app update install because native updater is unsupported.'); - return; - } - updater.quitAndInstall(false, true); - }, - }; -} -//# sourceMappingURL=app-updater.js.map diff --git a/changes/config-settings-controls.md b/changes/config-settings-controls.md index ea797aac..9f919be4 100644 --- a/changes/config-settings-controls.md +++ b/changes/config-settings-controls.md @@ -1,4 +1,4 @@ type: changed area: config -- Reorganized the Configuration window into clearer Appearance, Behavior, Anki, input, and integration sections with learned keybinding controls and AnkiConnect-backed deck, field, and note-type pickers. +- Reorganized the Settings window into clearer Appearance, Behavior, Anki, input, and integration sections with learned keybinding controls and AnkiConnect-backed deck, field, and note-type pickers. diff --git a/changes/config-settings-search.md b/changes/config-settings-search.md index 0300ec73..516993f9 100644 --- a/changes/config-settings-search.md +++ b/changes/config-settings-search.md @@ -1,4 +1,4 @@ type: fixed area: config -- Fixed Configuration window search so it searches across all categories, narrows on multi-word terms, hides settings owned by richer editors, and no longer shows the Open File button. +- Fixed Settings window search so it searches across all categories, narrows on multi-word terms, hides settings owned by richer editors, and no longer shows the Open File button. diff --git a/changes/config-settings-window.md b/changes/config-settings-window.md index c0c4b051..0b6bde3c 100644 --- a/changes/config-settings-window.md +++ b/changes/config-settings-window.md @@ -1,8 +1,8 @@ type: added area: config -- Added a dedicated Configuration window with launcher entry points via `subminer --config` and `subminer config`. -- Fixed the Configuration window preload so launcher-opened windows can initialize even when Electron sandboxing is active. -- Kept config-window startup lightweight by skipping AniList token refresh and automatic update polling. -- Marked safe live config options in the Configuration window and expanded hot reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki media/sentence/misc field mappings, sentence card model, and selected Anki annotation/runtime options. -- Hid AI and translation fields from the Configuration window while keeping them supported in config files. +- Added a dedicated Settings window with launcher entry points via `subminer --settings` and `subminer settings`. +- Fixed the Settings window preload so launcher-opened windows can initialize even when Electron sandboxing is active. +- Kept settings-window startup lightweight by skipping AniList token refresh and automatic update polling. +- Marked safe live config options in the Settings window and expanded hot reload for stats keys, logging level, Jimaku, Subsync, YouTube language defaults, Anki media/sentence/misc field mappings, sentence card model, and selected Anki annotation/runtime options. +- Hid AI and translation fields from the Settings window while keeping them supported in config files. diff --git a/changes/fix-known-words-decks-row-overflow.md b/changes/fix-known-words-decks-row-overflow.md new file mode 100644 index 00000000..467a358d --- /dev/null +++ b/changes/fix-known-words-decks-row-overflow.md @@ -0,0 +1,4 @@ +type: changed +area: config + +- Reorganized each known-words deck row in the Settings window into a card with the deck name on its own header line so longer deck names stay readable instead of being truncated. diff --git a/changes/fix-launcher-electron-menu-diagnostic.md b/changes/fix-launcher-electron-menu-diagnostic.md index 17e4f59b..de4cc3bc 100644 --- a/changes/fix-launcher-electron-menu-diagnostic.md +++ b/changes/fix-launcher-electron-menu-diagnostic.md @@ -1,4 +1,4 @@ type: fixed area: launcher -- Suppressed Electron macOS menu diagnostics from `subminer config` launcher output. +- Suppressed Electron macOS menu diagnostics from `subminer settings` launcher output. diff --git a/changes/linux-tray-appimage-update.md b/changes/linux-tray-appimage-update.md new file mode 100644 index 00000000..f6be21e0 --- /dev/null +++ b/changes/linux-tray-appimage-update.md @@ -0,0 +1,5 @@ +type: changed +area: updater + +- Linux tray "Check for Updates" now installs the new AppImage automatically via `electron-updater`, matching the macOS and Windows tray flow, instead of stopping at a "manual update required" dialog. AppImages managed by a system package (AUR `/opt/SubMiner/SubMiner.AppImage`) and non-AppImage launches (no `APPIMAGE` env) still fall back to the GitHub-asset flow. +- Routed `electron-updater` HTTP through `/usr/bin/curl` on Linux and disabled differential downloads, matching the macOS path, so background update checks stay off Electron's network service. diff --git a/changes/macos-config-window-exit.md b/changes/macos-config-window-exit.md new file mode 100644 index 00000000..3de4ba7c --- /dev/null +++ b/changes/macos-config-window-exit.md @@ -0,0 +1,4 @@ +type: fixed +area: launcher + +- macOS `subminer settings` launches now exit cleanly after the settings window is closed, returning control to the terminal without requiring Ctrl+C. diff --git a/changes/macos-update-dialog-activation.md b/changes/macos-update-dialog-activation.md new file mode 100644 index 00000000..72ea2d64 --- /dev/null +++ b/changes/macos-update-dialog-activation.md @@ -0,0 +1,4 @@ +type: fixed +area: updater + +- macOS update dialogs triggered by `subminer -u` now reliably appear in the foreground. SubMiner now shows the dock icon and activates itself via `osascript` (LaunchServices) before opening the modal alert; `app.focus({ steal: true })` alone was unreliable when SubMiner was reached through single-instance forwarding from the CLI-spawned child, leaving the dialog stranded behind other apps with a bouncing dock icon. diff --git a/changes/macos-updater-curl-fetch.md b/changes/macos-updater-curl-fetch.md new file mode 100644 index 00000000..7981b478 --- /dev/null +++ b/changes/macos-updater-curl-fetch.md @@ -0,0 +1,4 @@ +type: fixed +area: updater + +- Routed macOS supplemental GitHub release lookups through `/usr/bin/curl` instead of Electron `net.fetch`, eliminating the last Electron-networking path from background update checks and avoiding the network-service crashes seen in earlier prereleases. diff --git a/changes/rename-config-window-to-settings.md b/changes/rename-config-window-to-settings.md new file mode 100644 index 00000000..2eb171b4 --- /dev/null +++ b/changes/rename-config-window-to-settings.md @@ -0,0 +1,7 @@ +type: changed +area: launcher +breaking: true + +- Renamed the SubMiner Configuration window to the Settings window across the UI, tray menu, docs, and CLI verbiage. +- Replaced the `--config` flag and `subminer config` (no action) entry points with `--settings` and `subminer settings`. The `subminer config` subcommand now only accepts `path` or `show`. +- Removed the `--settings` alias that previously opened the bundled Yomitan settings popup. Use `--yomitan` to open Yomitan settings. diff --git a/changes/settings-modal-layout.md b/changes/settings-modal-layout.md new file mode 100644 index 00000000..15479a99 --- /dev/null +++ b/changes/settings-modal-layout.md @@ -0,0 +1 @@ +- Settings: reorganized playback, shortcut, WebSocket, tracking, Jellyfin, character dictionary, and Discord presence controls in the settings modal. diff --git a/changes/subtitle-css-live-settings.md b/changes/subtitle-css-live-settings.md index 70832c0e..132789f8 100644 --- a/changes/subtitle-css-live-settings.md +++ b/changes/subtitle-css-live-settings.md @@ -1,4 +1,4 @@ type: fixed area: config -- Fixed live Configuration window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays. +- Fixed live Settings window saves so primary and secondary subtitle CSS declarations apply immediately to open video overlays. diff --git a/docs-site/configuration.md b/docs-site/configuration.md index 0a26dec3..55893d4e 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -37,7 +37,7 @@ Then customize as needed using the sections below. ## Settings -SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--config` flag, or launcher commands such as `subminer --config` and `subminer config`. It is the primary way to configure SubMiner — all changes are written directly to `config.jsonc`, so manual file editing is not required for most users. +SubMiner includes a dedicated **Settings** window accessible from the tray menu, the app `--settings` flag, or launcher commands such as `subminer --settings` and `subminer settings`. It is the primary way to configure SubMiner — all changes are written directly to `config.jsonc`, so manual file editing is not required for most users. The Settings window groups options by workflow instead of mirroring the raw config-file shape: diff --git a/docs-site/installation.md b/docs-site/installation.md index 98562b33..78fa9d1d 100644 --- a/docs-site/installation.md +++ b/docs-site/installation.md @@ -300,9 +300,9 @@ subminer --update SubMiner verifies AppImage, launcher, and rofi theme downloads against `SHA256SUMS.txt`. If the binary is in a protected path, SubMiner shows the exact command to run rather than elevating itself. -On Linux, `subminer -u` performs the AppImage update from the launcher process directly. +The tray "Check for Updates" entry installs the new app automatically on Linux, macOS, and Windows. On Linux it replaces the running `.AppImage` in place via `electron-updater`; AppImages managed by a system package (for example the AUR `/opt/SubMiner/SubMiner.AppImage`) are skipped so the package manager stays in charge. -On macOS, tray update checks can also update the app automatically through Electron's built-in updater. +`subminer -u` also performs the AppImage update directly from the launcher process, which is useful when SubMiner is not currently running. ## How It All Fits Together diff --git a/docs-site/launcher-script.md b/docs-site/launcher-script.md index f1b71bd1..5c46524a 100644 --- a/docs-site/launcher-script.md +++ b/docs-site/launcher-script.md @@ -77,6 +77,7 @@ subminer stats -b # start background stats daemon | `subminer stats -b` | Start or reuse background stats daemon (non-blocking) | | `subminer stats cleanup` | Backfill vocabulary metadata and prune stale rows | | `subminer doctor` | Dependency + config + socket diagnostics | +| `subminer settings` | Open the SubMiner settings window | | `subminer config path` | Print active config file path | | `subminer config show` | Print active config contents | | `subminer mpv status` | Check mpv socket readiness | diff --git a/docs-site/troubleshooting.md b/docs-site/troubleshooting.md index 36bbc0db..df928ca8 100644 --- a/docs-site/troubleshooting.md +++ b/docs-site/troubleshooting.md @@ -205,7 +205,7 @@ If you installed from the AppImage and see this error, the package may be incomp **Yomitan lookup popup does not appear when hovering words or triggering lookup** - Verify Yomitan loaded successfully — check the terminal output for "Loaded Yomitan extension". -- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --settings`) and confirm at least one dictionary is imported. +- Yomitan requires dictionaries to be installed. Open Yomitan settings (`Alt+Shift+Y` or `SubMiner.AppImage --yomitan`) and confirm at least one dictionary is imported. - If `yomitan.externalProfilePath` is set, import/check dictionaries in the external app/profile instead. SubMiner treats that profile as read-only and does not open its own Yomitan settings window. - If the overlay shows subtitles but hover lookup never resolves on tokens, the tokenizer may have failed. See the MeCab section below. diff --git a/docs-site/usage.md b/docs-site/usage.md index fd5acef2..2df15602 100644 --- a/docs-site/usage.md +++ b/docs-site/usage.md @@ -131,7 +131,8 @@ SubMiner.AppImage --toggle-primary-subtitle-bar # Toggle primary subtitle SubMiner.AppImage --start --dev # Enable app/dev mode only SubMiner.AppImage --start --debug # Alias for --dev SubMiner.AppImage --start --log-level debug # Force verbose logging without app/dev mode -SubMiner.AppImage --settings # Open Yomitan settings +SubMiner.AppImage --yomitan # Open Yomitan settings +SubMiner.AppImage --settings # Open SubMiner settings window SubMiner.AppImage --jellyfin # Open Jellyfin setup window SubMiner.AppImage --jellyfin-login --jellyfin-server http://127.0.0.1:8096 --jellyfin-username me --jellyfin-password 'secret' SubMiner.AppImage --jellyfin-logout # Clear stored Jellyfin token/session data @@ -184,7 +185,8 @@ This flow requires `mpv.exe` to be discoverable. Leave `mpv.executablePath` blan - `subminer jellyfin` / `subminer jf`: Jellyfin-focused workflow aliases. - `subminer doctor`: health checks for core dependencies and runtime paths. -- `subminer config`: config helpers (`path`, `show`). +- `subminer settings`: open the SubMiner settings window (also `subminer --settings`). +- `subminer config`: config file helpers (`path`, `show`). - `subminer mpv`: mpv helpers (`status`, `socket`, `idle`). - `subminer dictionary `: generates a Yomitan-importable character dictionary ZIP from a file/directory target. - Use `subminer dictionary --candidates ` and `subminer dictionary --select ` to correct AniList character-dictionary matches for a whole series. @@ -264,7 +266,7 @@ secondary-sub-visibility=no SubMiner includes a bundled Yomitan extension for overlay word lookup. This bundled extension is separate from any Yomitan browser extension you may have installed. -For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --settings` or `SubMiner.AppImage --settings`) and import at least one dictionary in the bundled Yomitan instance. +For SubMiner overlay lookups to work, open Yomitan settings (`subminer app --yomitan` or `SubMiner.AppImage --yomitan`) and import at least one dictionary in the bundled Yomitan instance. If you also use Yomitan in a browser, configure that browser profile separately; it does not inherit dictionaries or settings from the bundled instance. diff --git a/launcher/commands/app-command.ts b/launcher/commands/app-command.ts index 4dd4c059..b6712437 100644 --- a/launcher/commands/app-command.ts +++ b/launcher/commands/app-command.ts @@ -6,8 +6,8 @@ export function runAppPassthroughCommand(context: LauncherCommandContext): boole if (!appPath) { return false; } - if (args.configSettings) { - runAppCommandWithInherit(appPath, ['--config']); + if (args.settings) { + runAppCommandWithInherit(appPath, ['--settings']); return true; } if (!args.appPassthrough) { diff --git a/launcher/commands/playback-command.test.ts b/launcher/commands/playback-command.test.ts index 6821ba54..23365e37 100644 --- a/launcher/commands/playback-command.test.ts +++ b/launcher/commands/playback-command.test.ts @@ -53,7 +53,7 @@ function createContext(): LauncherCommandContext { doctor: false, doctorRefreshKnownWords: false, version: false, - configSettings: false, + settings: false, configPath: false, configShow: false, mpvIdle: false, diff --git a/launcher/config/args-normalizer.test.ts b/launcher/config/args-normalizer.test.ts index a95daed0..fa8de87b 100644 --- a/launcher/config/args-normalizer.test.ts +++ b/launcher/config/args-normalizer.test.ts @@ -124,6 +124,7 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => { action: 'show', logLevel: 'warn', }, + settingsInvocation: null, mpvInvocation: null, appInvocation: null, dictionaryTriggered: false, @@ -159,13 +160,14 @@ test('applyInvocationsToArgs maps config and jellyfin invocation state', () => { assert.equal(parsed.logLevel, 'warn'); }); -test('applyInvocationsToArgs maps bare config invocation to settings window', () => { +test('applyInvocationsToArgs maps settings invocation to settings window', () => { const parsed = createDefaultArgs({}); applyInvocationsToArgs(parsed, { jellyfinInvocation: null, - configInvocation: { - action: undefined, + configInvocation: null, + settingsInvocation: { + logLevel: undefined, }, mpvInvocation: null, appInvocation: null, @@ -190,16 +192,54 @@ test('applyInvocationsToArgs maps bare config invocation to settings window', () texthookerOpenBrowser: false, }); - assert.equal(parsed.configSettings, true); + assert.equal(parsed.settings, true); assert.equal(parsed.configPath, false); }); +test('applyInvocationsToArgs fails when config invocation has no action', () => { + const parsed = createDefaultArgs({}); + + const error = withProcessExitIntercept(() => { + applyInvocationsToArgs(parsed, { + jellyfinInvocation: null, + configInvocation: { + action: undefined, + }, + settingsInvocation: null, + mpvInvocation: null, + appInvocation: null, + dictionaryTriggered: false, + dictionaryTarget: null, + dictionaryLogLevel: null, + dictionaryCandidates: false, + dictionarySelect: false, + dictionaryAnilistId: null, + statsTriggered: false, + statsBackground: false, + statsStop: false, + statsCleanup: false, + statsCleanupVocab: false, + statsCleanupLifetime: false, + statsLogLevel: null, + doctorTriggered: false, + doctorLogLevel: null, + doctorRefreshKnownWords: false, + texthookerTriggered: false, + texthookerLogLevel: null, + texthookerOpenBrowser: false, + }); + }); + + assert.equal(error.code, 1); +}); + test('applyInvocationsToArgs maps texthooker browser-open request', () => { const parsed = createDefaultArgs({}); applyInvocationsToArgs(parsed, { jellyfinInvocation: null, configInvocation: null, + settingsInvocation: null, mpvInvocation: null, appInvocation: null, dictionaryTriggered: false, diff --git a/launcher/config/args-normalizer.ts b/launcher/config/args-normalizer.ts index c06eb159..e828cba7 100644 --- a/launcher/config/args-normalizer.ts +++ b/launcher/config/args-normalizer.ts @@ -158,7 +158,7 @@ export function createDefaultArgs( doctorRefreshKnownWords: false, version: false, update: false, - configSettings: false, + settings: false, configPath: false, configShow: false, mpvIdle: false, @@ -222,7 +222,7 @@ export function applyRootOptionsToArgs( if (options.rofi === true) parsed.useRofi = true; if (options.update === true) parsed.update = true; if (options.version === true) parsed.version = true; - if (options.config === true) parsed.configSettings = true; + if (options.settings === true) parsed.settings = true; if (options.startOverlay === true) parsed.autoStartOverlay = true; if (options.texthooker === false) parsed.useTexthooker = false; if (typeof options.args === 'string') parsed.mpvArgs = options.args; @@ -311,10 +311,16 @@ export function applyInvocationsToArgs(parsed: Args, invocations: CliInvocations parsed.logLevel = parseLogLevel(invocations.configInvocation.logLevel); } const action = (invocations.configInvocation.action || '').toLowerCase(); - if (!action) parsed.configSettings = true; - else if (action === 'path') parsed.configPath = true; + if (action === 'path') parsed.configPath = true; else if (action === 'show') parsed.configShow = true; - else fail(`Unknown config action: ${invocations.configInvocation.action}`); + else fail(`Unknown config action: ${invocations.configInvocation.action || '(none)'}. Expected path or show.`); + } + + if (invocations.settingsInvocation) { + if (invocations.settingsInvocation.logLevel) { + parsed.logLevel = parseLogLevel(invocations.settingsInvocation.logLevel); + } + parsed.settings = true; } if (invocations.mpvInvocation) { diff --git a/launcher/config/cli-parser-builder.ts b/launcher/config/cli-parser-builder.ts index d0bf8f57..eaad4929 100644 --- a/launcher/config/cli-parser-builder.ts +++ b/launcher/config/cli-parser-builder.ts @@ -22,6 +22,7 @@ export interface CommandActionInvocation { export interface CliInvocations { jellyfinInvocation: JellyfinInvocation | null; configInvocation: CommandActionInvocation | null; + settingsInvocation: CommandActionInvocation | null; mpvInvocation: CommandActionInvocation | null; appInvocation: { appArgs: string[] } | null; dictionaryTriggered: boolean; @@ -58,7 +59,7 @@ function applyRootOptions(program: Command): void { .option('--start', 'Explicitly start overlay') .option('--log-level ', 'Log level') .option('-v, --version', 'Show SubMiner version') - .option('--config', 'Open configuration window') + .option('--settings', 'Open settings window') .option('-u, --update', 'Check for updates') .option('-R, --rofi', 'Use rofi picker') .option('-S, --start-overlay', 'Auto-start overlay') @@ -88,6 +89,7 @@ function getTopLevelCommand(argv: string[]): { name: string; index: number } | n 'jf', 'doctor', 'config', + 'settings', 'mpv', 'dictionary', 'dict', @@ -138,6 +140,7 @@ export function parseCliPrograms( } { let jellyfinInvocation: JellyfinInvocation | null = null; let configInvocation: CommandActionInvocation | null = null; + let settingsInvocation: CommandActionInvocation | null = null; let mpvInvocation: CommandActionInvocation | null = null; let appInvocation: { appArgs: string[] } | null = null; let dictionaryTriggered = false; @@ -293,7 +296,7 @@ export function parseCliPrograms( commandProgram .command('config') - .description('Config helpers') + .description('Config file helpers (path|show)') .argument('[action]', 'path|show') .option('--log-level ', 'Log level') .action((action: string | undefined, options: Record) => { @@ -303,6 +306,16 @@ export function parseCliPrograms( }; }); + commandProgram + .command('settings') + .description('Open SubMiner settings window') + .option('--log-level ', 'Log level') + .action((options: Record) => { + settingsInvocation = { + logLevel: typeof options.logLevel === 'string' ? options.logLevel : undefined, + }; + }); + commandProgram .command('mpv') .description('MPV helpers') @@ -356,6 +369,7 @@ export function parseCliPrograms( invocations: { jellyfinInvocation, configInvocation, + settingsInvocation, mpvInvocation, appInvocation, dictionaryTriggered, diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 5cd5c42a..b4ff4f53 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -232,7 +232,7 @@ test('doctor refresh-known-words forwards app refresh command without requiring }); }); -test('launcher config option forwards app configuration window command', () => { +test('launcher settings option forwards app settings window command', () => { withTempDir((root) => { const homeDir = path.join(root, 'home'); const xdgConfigHome = path.join(root, 'xdg'); @@ -249,14 +249,14 @@ test('launcher config option forwards app configuration window command', () => { SUBMINER_APPIMAGE_PATH: appPath, SUBMINER_TEST_CAPTURE: capturePath, }; - const result = runLauncher(['--config'], env); + const result = runLauncher(['--settings'], env); assert.equal(result.status, 0); - assert.equal(fs.readFileSync(capturePath, 'utf8'), '--config\n'); + assert.equal(fs.readFileSync(capturePath, 'utf8'), '--settings\n'); }); }); -test('launcher config command forwards app configuration window command', () => { +test('launcher settings command forwards app settings window command', () => { withTempDir((root) => { const homeDir = path.join(root, 'home'); const xdgConfigHome = path.join(root, 'xdg'); @@ -273,14 +273,14 @@ test('launcher config command forwards app configuration window command', () => SUBMINER_APPIMAGE_PATH: appPath, SUBMINER_TEST_CAPTURE: capturePath, }; - const result = runLauncher(['config'], env); + const result = runLauncher(['settings'], env); assert.equal(result.status, 0); - assert.equal(fs.readFileSync(capturePath, 'utf8'), '--config\n'); + assert.equal(fs.readFileSync(capturePath, 'utf8'), '--settings\n'); }); }); -test('launcher config command suppresses known Electron macOS menu diagnostics', () => { +test('launcher settings command suppresses known Electron macOS menu diagnostics', () => { withTempDir((root) => { const homeDir = path.join(root, 'home'); const xdgConfigHome = path.join(root, 'xdg'); @@ -301,7 +301,7 @@ test('launcher config command suppresses known Electron macOS menu diagnostics', ...makeTestEnv(homeDir, xdgConfigHome), SUBMINER_APPIMAGE_PATH: appPath, }; - const result = runLauncher(['config'], env); + const result = runLauncher(['settings'], env); assert.equal(result.status, 0); assert.equal(result.stderr, 'real stderr line\n'); diff --git a/launcher/mpv.test.ts b/launcher/mpv.test.ts index 61339175..2e1a621b 100644 --- a/launcher/mpv.test.ts +++ b/launcher/mpv.test.ts @@ -569,7 +569,7 @@ function makeArgs(overrides: Partial = {}): Args { doctor: false, doctorRefreshKnownWords: false, version: false, - configSettings: false, + settings: false, configPath: false, configShow: false, mpvIdle: false, diff --git a/launcher/parse-args.test.ts b/launcher/parse-args.test.ts index e704f840..0e2d5f8d 100644 --- a/launcher/parse-args.test.ts +++ b/launcher/parse-args.test.ts @@ -57,10 +57,10 @@ test('parseArgs captures mpv args string', () => { assert.equal(parsed.mpvArgs, '--pause=yes --title="movie night"'); }); -test('parseArgs maps root config window option', () => { - const parsed = parseArgs(['--config'], 'subminer', {}); +test('parseArgs maps root settings window option', () => { + const parsed = parseArgs(['--settings'], 'subminer', {}); - assert.equal(parsed.configSettings, true); + assert.equal(parsed.settings, true); }); test('parseArgs maps root update flags without conflicting with jellyfin username', () => { @@ -107,10 +107,10 @@ test('parseArgs maps config show action', () => { assert.equal(parsed.configPath, false); }); -test('parseArgs maps bare config command to settings window', () => { - const parsed = parseArgs(['config'], 'subminer', {}); +test('parseArgs maps settings command to settings window', () => { + const parsed = parseArgs(['settings'], 'subminer', {}); - assert.equal(parsed.configSettings, true); + assert.equal(parsed.settings, true); assert.equal(parsed.configPath, false); assert.equal(parsed.configShow, false); }); @@ -119,7 +119,7 @@ test('parseArgs maps config path action to config path output', () => { const parsed = parseArgs(['config', 'path'], 'subminer', {}); assert.equal(parsed.configPath, true); - assert.equal(parsed.configSettings, false); + assert.equal(parsed.settings, false); }); test('parseArgs rejects removed config open and launch actions', () => { @@ -134,6 +134,14 @@ test('parseArgs rejects removed config open and launch actions', () => { assert.equal(exit.code, 1); }); +test('parseArgs requires an explicit action for the config subcommand', () => { + const exit = withProcessExitIntercept(() => { + parseArgs(['config'], 'subminer', {}); + }); + + assert.equal(exit.code, 1); +}); + test('parseArgs maps mpv idle action', () => { const parsed = parseArgs(['mpv', 'idle'], 'subminer', {}); diff --git a/launcher/types.ts b/launcher/types.ts index 742886c1..b156027b 100644 --- a/launcher/types.ts +++ b/launcher/types.ts @@ -133,7 +133,7 @@ export interface Args { doctorRefreshKnownWords: boolean; version: boolean; update?: boolean; - configSettings: boolean; + settings: boolean; configPath: boolean; configShow: boolean; mpvIdle: boolean; diff --git a/main.js b/main.js deleted file mode 100644 index 6394f3c5..00000000 --- a/main.js +++ /dev/null @@ -1,4711 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -/* - SubMiner - All-in-one sentence mining overlay - Copyright (C) 2024 sudacode - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ -const electron_1 = require("electron"); -const controller_config_update_js_1 = require("./main/controller-config-update.js"); -const playlist_browser_open_1 = require("./main/runtime/playlist-browser-open"); -const discord_rpc_client_js_1 = require("./main/runtime/discord-rpc-client.js"); -const linux_mpv_fullscreen_overlay_refresh_1 = require("./main/runtime/linux-mpv-fullscreen-overlay-refresh"); -const config_1 = require("./ai/config"); -function getPasswordStoreArg(argv) { - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - if (!arg?.startsWith('--password-store')) { - continue; - } - if (arg === '--password-store') { - const value = argv[i + 1]; - if (value && !value.startsWith('--')) { - return value; - } - return null; - } - const [prefix, value] = arg.split('=', 2); - if (prefix === '--password-store' && value && value.trim().length > 0) { - return value.trim(); - } - } - return null; -} -function normalizePasswordStoreArg(value) { - const normalized = value.trim(); - if (normalized.toLowerCase() === 'gnome') { - return 'gnome-libsecret'; - } - return normalized; -} -function getDefaultPasswordStore() { - return 'gnome-libsecret'; -} -electron_1.protocol.registerSchemesAsPrivileged([ - { - scheme: 'chrome-extension', - privileges: { - standard: true, - secure: true, - supportFetchAPI: true, - corsEnabled: true, - bypassCSP: true, - }, - }, -]); -const fs = __importStar(require("fs")); -const node_child_process_1 = require("node:child_process"); -const os = __importStar(require("os")); -const path = __importStar(require("path")); -const mecab_tokenizer_1 = require("./mecab-tokenizer"); -const anki_integration_1 = require("./anki-integration"); -const subtitle_timing_tracker_1 = require("./subtitle-timing-tracker"); -const runtime_options_1 = require("./runtime-options"); -const utils_1 = require("./jimaku/utils"); -const logger_1 = require("./logger"); -const window_trackers_1 = require("./window-trackers"); -const windows_helper_1 = require("./window-trackers/windows-helper"); -const args_1 = require("./cli/args"); -const help_1 = require("./cli/help"); -const contracts_1 = require("./shared/ipc/contracts"); -const anki_connect_1 = require("./anki-connect"); -const startup_mode_flags_1 = require("./main/runtime/startup-mode-flags"); -const config_validation_1 = require("./main/config-validation"); -const anilist_1 = require("./main/runtime/domains/anilist"); -const watch_threshold_1 = require("./shared/watch-threshold"); -const jellyfin_1 = require("./main/runtime/domains/jellyfin"); -const overlay_1 = require("./main/runtime/domains/overlay"); -const startup_1 = require("./main/runtime/domains/startup"); -const mpv_1 = require("./main/runtime/domains/mpv"); -const mining_1 = require("./main/runtime/domains/mining"); -const utils_2 = require("./core/utils"); -const setup_state_1 = require("./shared/setup-state"); -const services_1 = require("./core/services"); -const generate_1 = require("./core/services/youtube/generate"); -const playback_resolve_1 = require("./core/services/youtube/playback-resolve"); -const track_probe_1 = require("./core/services/youtube/track-probe"); -const stats_server_1 = require("./core/services/stats-server"); -const stats_window_js_1 = require("./core/services/stats-window.js"); -const stats_window_js_2 = require("./core/services/stats-window.js"); -const first_run_setup_service_1 = require("./main/runtime/first-run-setup-service"); -const youtube_flow_1 = require("./main/runtime/youtube-flow"); -const youtube_playback_runtime_1 = require("./main/runtime/youtube-playback-runtime"); -const youtube_primary_subtitle_notification_1 = require("./main/runtime/youtube-primary-subtitle-notification"); -const autoplay_ready_gate_1 = require("./main/runtime/autoplay-ready-gate"); -const local_subtitle_selection_1 = require("./main/runtime/local-subtitle-selection"); -const first_run_setup_window_1 = require("./main/runtime/first-run-setup-window"); -const first_run_setup_plugin_1 = require("./main/runtime/first-run-setup-plugin"); -const windows_mpv_shortcuts_1 = require("./main/runtime/windows-mpv-shortcuts"); -const command_line_launcher_1 = require("./main/runtime/command-line-launcher"); -const windows_mpv_launch_1 = require("./main/runtime/windows-mpv-launch"); -const jellyfin_remote_connection_1 = require("./main/runtime/jellyfin-remote-connection"); -const jellyfin_tray_discovery_1 = require("./main/runtime/jellyfin-tray-discovery"); -const youtube_playback_launch_1 = require("./main/runtime/youtube-playback-launch"); -const startup_tray_policy_1 = require("./main/runtime/startup-tray-policy"); -const immersion_startup_1 = require("./main/runtime/immersion-startup"); -const immersion_startup_main_deps_1 = require("./main/runtime/immersion-startup-main-deps"); -const stats_cli_command_1 = require("./main/runtime/stats-cli-command"); -const stats_daemon_1 = require("./main/runtime/stats-daemon"); -const stats_server_routing_1 = require("./main/runtime/stats-server-routing"); -const legacy_vocabulary_pos_1 = require("./core/services/immersion-tracker/legacy-vocabulary-pos"); -const anilist_update_queue_1 = require("./core/services/anilist/anilist-update-queue"); -const anilist_updater_1 = require("./core/services/anilist/anilist-updater"); -const cover_art_fetcher_1 = require("./core/services/anilist/cover-art-fetcher"); -const rate_limiter_1 = require("./core/services/anilist/rate-limiter"); -const jellyfin_token_store_1 = require("./core/services/jellyfin-token-store"); -const runtime_options_ipc_1 = require("./core/services/runtime-options-ipc"); -const anilist_token_store_1 = require("./core/services/anilist/anilist-token-store"); -const session_bindings_1 = require("./core/services/session-bindings"); -const session_actions_1 = require("./core/services/session-actions"); -const shortcuts_1 = require("./main/runtime/domains/shortcuts"); -const registry_1 = require("./main/runtime/registry"); -const overlay_mpv_sub_visibility_1 = require("./main/runtime/overlay-mpv-sub-visibility"); -const composers_1 = require("./main/runtime/composers"); -const overlay_window_runtime_handlers_1 = require("./main/runtime/overlay-window-runtime-handlers"); -const startup_2 = require("./main/startup"); -const startup_lifecycle_1 = require("./main/startup-lifecycle"); -const early_single_instance_1 = require("./main/early-single-instance"); -const ipc_mpv_command_1 = require("./main/ipc-mpv-command"); -const ipc_runtime_1 = require("./main/ipc-runtime"); -const dependencies_1 = require("./main/dependencies"); -const services_2 = require("./main/boot/services"); -const cli_runtime_1 = require("./main/cli-runtime"); -const overlay_runtime_1 = require("./main/overlay-runtime"); -const overlay_modal_input_state_1 = require("./main/runtime/overlay-modal-input-state"); -const youtube_picker_open_1 = require("./main/runtime/youtube-picker-open"); -const runtime_options_open_1 = require("./main/runtime/runtime-options-open"); -const jimaku_open_1 = require("./main/runtime/jimaku-open"); -const subsync_open_1 = require("./main/runtime/subsync-open"); -const session_help_open_1 = require("./main/runtime/session-help-open"); -const character_dictionary_open_1 = require("./main/runtime/character-dictionary-open"); -const controller_select_open_1 = require("./main/runtime/controller-select-open"); -const controller_debug_open_1 = require("./main/runtime/controller-debug-open"); -const playlist_browser_ipc_1 = require("./main/runtime/playlist-browser-ipc"); -const session_bindings_artifact_1 = require("./main/runtime/session-bindings-artifact"); -const overlay_shortcuts_runtime_1 = require("./main/overlay-shortcuts-runtime"); -const frequency_dictionary_runtime_1 = require("./main/frequency-dictionary-runtime"); -const jlpt_runtime_1 = require("./main/jlpt-runtime"); -const media_runtime_1 = require("./main/media-runtime"); -const overlay_visibility_runtime_1 = require("./main/overlay-visibility-runtime"); -const discord_presence_runtime_1 = require("./main/runtime/discord-presence-runtime"); -const character_dictionary_runtime_1 = require("./main/character-dictionary-runtime"); -const character_dictionary_auto_sync_1 = require("./main/runtime/character-dictionary-auto-sync"); -const character_dictionary_auto_sync_completion_1 = require("./main/runtime/character-dictionary-auto-sync-completion"); -const character_dictionary_auto_sync_notifications_1 = require("./main/runtime/character-dictionary-auto-sync-notifications"); -const current_media_tokenization_gate_1 = require("./main/runtime/current-media-tokenization-gate"); -const current_subtitle_snapshot_1 = require("./main/runtime/current-subtitle-snapshot"); -const startup_osd_sequencer_1 = require("./main/runtime/startup-osd-sequencer"); -const app_updater_1 = require("./main/runtime/update/app-updater"); -const fetch_adapter_1 = require("./main/runtime/update/fetch-adapter"); -const curl_http_executor_1 = require("./main/runtime/update/curl-http-executor"); -const release_assets_1 = require("./main/runtime/update/release-assets"); -const release_metadata_policy_1 = require("./main/runtime/update/release-metadata-policy"); -const launcher_updater_1 = require("./main/runtime/update/launcher-updater"); -const update_notifications_1 = require("./main/runtime/update/update-notifications"); -const update_dialogs_1 = require("./main/runtime/update/update-dialogs"); -const update_cli_command_1 = require("./main/runtime/update/update-cli-command"); -const update_service_1 = require("./main/runtime/update/update-service"); -const support_assets_1 = require("./main/runtime/update/support-assets"); -const subtitle_prefetch_runtime_1 = require("./main/runtime/subtitle-prefetch-runtime"); -const setup_window_factory_1 = require("./main/runtime/setup-window-factory"); -const config_settings_runtime_1 = require("./main/runtime/config-settings-runtime"); -const youtube_playback_1 = require("./main/runtime/youtube-playback"); -const yomitan_profile_policy_1 = require("./main/runtime/yomitan-profile-policy"); -const yomitan_read_only_log_1 = require("./main/runtime/yomitan-read-only-log"); -const yomitan_anki_server_1 = require("./main/runtime/yomitan-anki-server"); -const state_1 = require("./main/state"); -const anilist_url_guard_1 = require("./main/anilist-url-guard"); -const config_2 = require("./config"); -const path_resolution_1 = require("./config/path-resolution"); -const registry_2 = require("./config/settings/registry"); -const subtitle_cue_parser_1 = require("./core/services/subtitle-cue-parser"); -const subtitle_prefetch_1 = require("./core/services/subtitle-prefetch"); -const subtitle_prefetch_source_1 = require("./main/runtime/subtitle-prefetch-source"); -const subtitle_prefetch_init_1 = require("./main/runtime/subtitle-prefetch-init"); -const character_dictionary_selection_1 = require("./main/character-dictionary-selection"); -const utils_3 = require("./subsync/utils"); -if (process.platform === 'linux') { - electron_1.app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal'); - const passwordStore = normalizePasswordStoreArg(getPasswordStoreArg(process.argv) ?? getDefaultPasswordStore()); - electron_1.app.commandLine.appendSwitch('password-store', passwordStore); - (0, logger_1.createLogger)('main').debug(`Applied --password-store ${passwordStore}`); -} -electron_1.app.setName('SubMiner'); -const DEFAULT_TEXTHOOKER_PORT = 5174; -const DEFAULT_MPV_LOG_FILE = (0, logger_1.resolveDefaultLogFilePath)({ - platform: process.platform, - homeDir: os.homedir(), - appDataDir: process.env.APPDATA, -}); -const ANILIST_SETUP_CLIENT_ID_URL = 'https://anilist.co/api/v2/oauth/authorize'; -const ANILIST_SETUP_RESPONSE_TYPE = 'token'; -const ANILIST_DEFAULT_CLIENT_ID = '36084'; -const ANILIST_REDIRECT_URI = 'https://anilist.subminer.moe/'; -const ANILIST_DEVELOPER_SETTINGS_URL = 'https://anilist.co/settings/developer'; -const ANILIST_UPDATE_MIN_WATCH_SECONDS = 10 * 60; -const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000; -const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000; -const TRAY_TOOLTIP = 'SubMiner'; -let anilistMediaGuessRuntimeState = (0, state_1.createInitialAnilistMediaGuessRuntimeState)(); -let anilistUpdateInFlightState = (0, state_1.createInitialAnilistUpdateInFlightState)(); -const anilistAttemptedUpdateKeys = new Set(); -let anilistCachedAccessToken = null; -let jellyfinPlayQuitOnDisconnectArmed = false; -const JELLYFIN_LANG_PREF = 'ja,jp,jpn,japanese,en,eng,english,enUS,en-US'; -const JELLYFIN_TICKS_PER_SECOND = 10_000_000; -const JELLYFIN_REMOTE_PROGRESS_INTERVAL_MS = 3000; -const DISCORD_PRESENCE_APP_ID = '1475264834730856619'; -const JELLYFIN_MPV_CONNECT_TIMEOUT_MS = 3000; -const JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS = 20000; -const YOUTUBE_MPV_CONNECT_TIMEOUT_MS = 3000; -const YOUTUBE_MPV_AUTO_LAUNCH_TIMEOUT_MS = 10000; -const YOUTUBE_MPV_YTDL_FORMAT = 'bestvideo*+bestaudio/best'; -const YOUTUBE_DIRECT_PLAYBACK_FORMAT = 'b'; -const MPV_JELLYFIN_DEFAULT_ARGS = [ - '--sub-auto=fuzzy', - '--sub-file-paths=.;subs;subtitles', - '--sid=auto', - '--secondary-sid=auto', - '--secondary-sub-visibility=no', - '--alang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', - '--slang=ja,jp,jpn,japanese,en,eng,english,enus,en-us', -]; -let activeJellyfinRemotePlayback = null; -let jellyfinRemoteLastProgressAtMs = 0; -let jellyfinMpvAutoLaunchInFlight = null; -let backgroundWarmupsStarted = false; -let yomitanLoadInFlight = null; -let notifyAnilistTokenStoreWarning = () => { }; -const buildApplyJellyfinMpvDefaultsMainDepsHandler = (0, jellyfin_1.createBuildApplyJellyfinMpvDefaultsMainDepsHandler)({ - sendMpvCommandRuntime: (client, command) => (0, services_1.sendMpvCommandRuntime)(client, command), - jellyfinLangPref: JELLYFIN_LANG_PREF, -}); -const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler(); -const applyJellyfinMpvDefaultsHandler = (0, jellyfin_1.createApplyJellyfinMpvDefaultsHandler)(applyJellyfinMpvDefaultsMainDeps); -function applyJellyfinMpvDefaults(client) { - applyJellyfinMpvDefaultsHandler(client); -} -const isDev = process.argv.includes('--dev') || process.argv.includes('--debug'); -const texthookerService = new services_1.Texthooker(() => { - const config = getResolvedConfig(); - const characterDictionaryEnabled = config.anilist.characterDictionary.enabled && - yomitanProfilePolicy.isCharacterDictionaryEnabled(); - const knownWordColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled); - const nPlusOneColoringEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); - return { - enableKnownWordColoring: knownWordColoringEnabled, - enableNPlusOneColoring: nPlusOneColoringEnabled, - enableNameMatchColoring: config.subtitleStyle.nameMatchEnabled && characterDictionaryEnabled, - enableFrequencyColoring: getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled), - enableJlptColoring: getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt), - characterDictionaryEnabled, - knownWordColor: config.subtitleStyle.knownWordColor, - nPlusOneColor: config.subtitleStyle.nPlusOneColor, - nameMatchColor: config.subtitleStyle.nameMatchColor, - hoverTokenColor: config.subtitleStyle.hoverTokenColor, - hoverTokenBackgroundColor: config.subtitleStyle.hoverTokenBackgroundColor, - jlptColors: config.subtitleStyle.jlptColors, - frequencyDictionary: { - singleColor: config.subtitleStyle.frequencyDictionary.singleColor, - bandedColors: config.subtitleStyle.frequencyDictionary.bandedColors, - }, - }; -}); -let syncOverlayShortcutsForModal = () => { }; -let syncOverlayVisibilityForModal = () => { }; -const buildGetDefaultSocketPathMainDepsHandler = (0, jellyfin_1.createBuildGetDefaultSocketPathMainDepsHandler)({ - platform: process.platform, -}); -const getDefaultSocketPathMainDeps = buildGetDefaultSocketPathMainDepsHandler(); -const getDefaultSocketPathHandler = (0, jellyfin_1.createGetDefaultSocketPathHandler)(getDefaultSocketPathMainDeps); -function getDefaultSocketPath() { - return getDefaultSocketPathHandler(); -} -const bootServices = (0, services_2.createMainBootServices)({ - platform: process.platform, - argv: process.argv, - appDataDir: process.env.APPDATA, - xdgConfigHome: process.env.XDG_CONFIG_HOME, - homeDir: os.homedir(), - defaultMpvLogFile: DEFAULT_MPV_LOG_FILE, - envMpvLog: process.env.SUBMINER_MPV_LOG, - defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT, - getDefaultSocketPath: () => getDefaultSocketPath(), - resolveConfigDir: path_resolution_1.resolveConfigDir, - existsSync: fs.existsSync, - mkdirSync: fs.mkdirSync, - joinPath: (...parts) => path.join(...parts), - app: electron_1.app, - shouldBypassSingleInstanceLock: () => (0, early_single_instance_1.shouldBypassSingleInstanceLockForArgv)(process.argv), - requestSingleInstanceLockEarly: () => (0, early_single_instance_1.requestSingleInstanceLockEarly)(electron_1.app), - registerSecondInstanceHandlerEarly: (listener) => { - (0, early_single_instance_1.registerSecondInstanceHandlerEarly)(electron_1.app, listener); - }, - onConfigStartupParseError: (error) => { - (0, config_validation_1.failStartupFromConfig)('SubMiner config parse error', (0, config_validation_1.buildConfigParseErrorDetails)(error.path, error.parseError), { - logError: (details) => console.error(details), - showErrorBox: (title, details) => electron_1.dialog.showErrorBox(title, details), - quit: () => requestAppQuit(), - }); - }, - createConfigService: (configDir) => new config_2.ConfigService(configDir), - createAnilistTokenStore: (targetPath) => (0, anilist_token_store_1.createAnilistTokenStore)(targetPath, { - info: (message) => console.info(message), - warn: (message, details) => console.warn(message, details), - error: (message, details) => console.error(message, details), - warnUser: (message) => notifyAnilistTokenStoreWarning(message), - }), - createJellyfinTokenStore: (targetPath) => (0, jellyfin_token_store_1.createJellyfinTokenStore)(targetPath, { - info: (message) => console.info(message), - warn: (message, details) => console.warn(message, details), - error: (message, details) => console.error(message, details), - }), - createAnilistUpdateQueue: (targetPath) => (0, anilist_update_queue_1.createAnilistUpdateQueue)(targetPath, { - info: (message) => console.info(message), - warn: (message, details) => console.warn(message, details), - error: (message, details) => console.error(message, details), - }), - createSubtitleWebSocket: () => new services_1.SubtitleWebSocket(), - createLogger: logger_1.createLogger, - createMainRuntimeRegistry: registry_1.createMainRuntimeRegistry, - createOverlayManager: services_1.createOverlayManager, - createOverlayModalInputState: overlay_modal_input_state_1.createOverlayModalInputState, - createOverlayContentMeasurementStore: ({ logger }) => { - const buildHandler = (0, overlay_1.createBuildOverlayContentMeasurementStoreMainDepsHandler)({ - now: () => Date.now(), - warn: (message) => logger.warn(message), - }); - return (0, services_1.createOverlayContentMeasurementStore)(buildHandler()); - }, - getSyncOverlayShortcutsForModal: () => syncOverlayShortcutsForModal, - getSyncOverlayVisibilityForModal: () => syncOverlayVisibilityForModal, - createOverlayModalRuntime: ({ overlayManager, overlayModalInputState }) => { - const buildHandler = (0, overlay_1.createBuildOverlayModalRuntimeMainDepsHandler)({ - getMainWindow: () => overlayManager.getMainWindow(), - getModalWindow: () => overlayManager.getModalWindow(), - createModalWindow: () => createModalWindow(), - getModalGeometry: () => getCurrentOverlayGeometry(), - setModalWindowBounds: (geometry) => overlayManager.setModalWindowBounds(geometry), - }); - return (0, overlay_runtime_1.createOverlayModalRuntimeService)(buildHandler(), { - onModalStateChange: (isActive) => overlayModalInputState.handleModalInputStateChange(isActive), - }); - }, - createAppState: state_1.createAppState, -}); -const { configDir: CONFIG_DIR, userDataPath: USER_DATA_PATH, defaultMpvLogPath: DEFAULT_MPV_LOG_PATH, defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH, configService, anilistTokenStore, jellyfinTokenStore, anilistUpdateQueue, subtitleWsService, annotationSubtitleWsService, logger, runtimeRegistry, overlayManager, overlayModalInputState, overlayContentMeasurementStore, overlayModalRuntime, appState, appLifecycleApp, } = bootServices; -const configSettingsFields = (0, registry_2.buildConfigSettingsRegistry)(config_2.DEFAULT_CONFIG); -notifyAnilistTokenStoreWarning = (message) => { - logger.warn(`[AniList] ${message}`); - try { - (0, utils_2.showDesktopNotification)('SubMiner AniList', { - body: message, - }); - } - catch { - // Notification may fail if desktop notifications are unavailable early in startup. - } -}; -const appLogger = { - logInfo: (message) => { - logger.info(message); - }, - logDebug: (message) => { - logger.debug(message); - }, - logWarning: (message) => { - logger.warn(message); - }, - logError: (message, details) => { - logger.error(message, details); - }, - logNoRunningInstance: () => { - logger.error('No running instance. Use --start to launch the app.'); - }, - logConfigWarning: (warning) => { - logger.warn(`[config] ${warning.path}: ${warning.message} value=${JSON.stringify(warning.value)} fallback=${JSON.stringify(warning.fallback)}`); - }, -}; -let forceQuitTimer = null; -let statsServer = null; -const statsDaemonStatePath = path.join(USER_DATA_PATH, 'stats-daemon.json'); -function readLiveBackgroundStatsDaemonState() { - const state = (0, stats_daemon_1.readBackgroundStatsServerState)(statsDaemonStatePath); - if (!state) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return null; - } - if (state.pid === process.pid && !statsServer) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return null; - } - if (!(0, stats_daemon_1.isBackgroundStatsServerProcessAlive)(state.pid)) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return null; - } - return state; -} -function clearOwnedBackgroundStatsDaemonState() { - const state = (0, stats_daemon_1.readBackgroundStatsServerState)(statsDaemonStatePath); - if (state?.pid === process.pid) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - } -} -function stopStatsServer() { - if (!statsServer) { - return; - } - statsServer.close(); - statsServer = null; - clearOwnedBackgroundStatsDaemonState(); -} -function requestAppQuit() { - (0, services_1.destroyYomitanSettingsWindow)(appState.yomitanSettingsWindow); - appState.yomitanSettingsWindow = null; - (0, stats_window_js_1.destroyStatsWindow)(); - stopStatsServer(); - if (!forceQuitTimer) { - forceQuitTimer = setTimeout(() => { - logger.warn('App quit timed out; forcing process exit.'); - electron_1.app.exit(0); - }, 2000); - } - electron_1.app.quit(); -} -process.on('SIGINT', () => { - requestAppQuit(); -}); -process.on('SIGTERM', () => { - requestAppQuit(); -}); -const startBackgroundWarmupsIfAllowed = () => { - startBackgroundWarmups(); -}; -const youtubeFlowRuntime = (0, youtube_flow_1.createYoutubeFlowRuntime)({ - probeYoutubeTracks: (url) => (0, track_probe_1.probeYoutubeTracks)(url), - acquireYoutubeSubtitleTrack: (input) => (0, generate_1.acquireYoutubeSubtitleTrack)(input), - acquireYoutubeSubtitleTracks: (input) => (0, generate_1.acquireYoutubeSubtitleTracks)(input), - openPicker: async (payload) => { - return await (0, youtube_picker_open_1.openYoutubeTrackPicker)({ - sendToActiveOverlayWindow: (channel, nextPayload, runtimeOptions) => overlayModalRuntime.sendToActiveOverlayWindow(channel, nextPayload, runtimeOptions), - waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs), - logWarn: (message) => logger.warn(message), - }, payload); - }, - pauseMpv: () => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['set_property', 'pause', 'yes']); - }, - resumeMpv: () => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['set_property', 'pause', 'no']); - }, - sendMpvCommand: (command) => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command); - }, - requestMpvProperty: async (name) => { - const client = appState.mpvClient; - if (!client) - return null; - return await client.requestProperty(name); - }, - refreshCurrentSubtitle: (text) => { - subtitleProcessingController.refreshCurrentSubtitle(text); - }, - refreshSubtitleSidebarSource: async (sourcePath) => { - await subtitlePrefetchRuntime.refreshSubtitleSidebarFromSource(sourcePath); - }, - startTokenizationWarmups: async () => { - await startTokenizationWarmups(); - }, - waitForTokenizationReady: async () => { - await currentMediaTokenizationGate.waitUntilReady(appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null); - }, - waitForAnkiReady: async () => { - const integration = appState.ankiIntegration; - if (!integration) { - return; - } - try { - await Promise.race([ - integration.waitUntilReady(), - new Promise((_, reject) => { - setTimeout(() => reject(new Error('Timed out waiting for AnkiConnect integration')), 2500); - }), - ]); - } - catch (error) { - logger.warn('Continuing YouTube playback before AnkiConnect integration reported ready:', error instanceof Error ? error.message : String(error)); - } - }, - wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - waitForPlaybackWindowReady: async () => { - const deadline = Date.now() + 4000; - let stableGeometry = null; - let stableSinceMs = 0; - while (Date.now() < deadline) { - const tracker = appState.windowTracker; - const trackerGeometry = tracker?.getGeometry() ?? null; - const mediaPath = appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || ''; - const trackerFocused = tracker?.isTargetWindowFocused() ?? false; - if (tracker && tracker.isTracking() && trackerGeometry && trackerFocused && mediaPath) { - if (!geometryMatches(stableGeometry, trackerGeometry)) { - stableGeometry = trackerGeometry; - stableSinceMs = Date.now(); - } - else if (Date.now() - stableSinceMs >= 200) { - return; - } - } - else { - stableGeometry = null; - stableSinceMs = 0; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - logger.warn('Timed out waiting for tracked playback window focus/media readiness before opening YouTube subtitle picker.'); - }, - waitForOverlayGeometryReady: async () => { - const deadline = Date.now() + 4000; - while (Date.now() < deadline) { - const tracker = appState.windowTracker; - const trackerGeometry = tracker?.getGeometry() ?? null; - if (trackerGeometry && geometryMatches(lastOverlayWindowGeometry, trackerGeometry)) { - return; - } - await new Promise((resolve) => setTimeout(resolve, 50)); - } - logger.warn('Timed out waiting for overlay geometry to match tracked playback window.'); - }, - focusOverlayWindow: () => { - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - return; - } - mainWindow.setIgnoreMouseEvents(false); - if (!mainWindow.isFocused()) { - mainWindow.focus(); - } - if (!mainWindow.webContents.isFocused()) { - mainWindow.webContents.focus(); - } - }, - showMpvOsd: (text) => showMpvOsd(text), - reportSubtitleFailure: (message) => reportYoutubeSubtitleFailure(message), - warn: (message) => logger.warn(message), - log: (message) => logger.info(message), - getYoutubeOutputDir: () => path.join(os.homedir(), '.cache', 'subminer', 'youtube-subs'), -}); -const prepareYoutubePlaybackInMpv = (0, youtube_playback_launch_1.createPrepareYoutubePlaybackInMpvHandler)({ - requestPath: async () => { - const client = appState.mpvClient; - if (!client) - return null; - const value = await client.requestProperty('path').catch(() => null); - return typeof value === 'string' ? value : null; - }, - requestProperty: async (name) => { - const client = appState.mpvClient; - if (!client) - return null; - return await client.requestProperty(name); - }, - sendMpvCommand: (command) => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command); - }, - wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), -}); -const waitForYoutubeMpvConnected = (0, jellyfin_remote_connection_1.createWaitForMpvConnectedHandler)({ - getMpvClient: () => appState.mpvClient, - now: () => Date.now(), - sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)), -}); -const autoplayReadyGate = (0, autoplay_ready_gate_1.createAutoplayReadyGate)({ - isAppOwnedFlowInFlight: () => youtubePrimarySubtitleNotificationRuntime.isAppOwnedFlowInFlight(), - getCurrentMediaPath: () => appState.currentMediaPath, - getCurrentVideoPath: () => appState.mpvClient?.currentVideoPath ?? null, - getPlaybackPaused: () => appState.playbackPaused, - getMpvClient: () => appState.mpvClient, - signalPluginAutoplayReady: () => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['script-message', 'subminer-autoplay-ready']); - }, - schedule: (callback, delayMs) => setTimeout(callback, delayMs), - logDebug: (message) => logger.debug(message), -}); -const managedLocalSubtitleSelectionRuntime = (0, local_subtitle_selection_1.createManagedLocalSubtitleSelectionRuntime)({ - getCurrentMediaPath: () => appState.currentMediaPath, - getMpvClient: () => appState.mpvClient, - getPrimarySubtitleLanguages: () => getResolvedConfig().youtube.primarySubLanguages, - getSecondarySubtitleLanguages: () => getResolvedConfig().secondarySub.secondarySubLanguages, - sendMpvCommand: (command) => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command); - }, - schedule: (callback, delayMs) => setTimeout(callback, delayMs), - clearScheduled: (timer) => clearTimeout(timer), -}); -function resolveBundledMpvRuntimePluginEntrypoint() { - return ((0, first_run_setup_plugin_1.resolvePackagedRuntimePluginPath)({ - dirname: __dirname, - appPath: electron_1.app.getAppPath(), - resourcesPath: process.resourcesPath, - }) ?? undefined); -} -function detectWindowsInstalledMpvPlugin(mpvExecutablePath) { - return (0, first_run_setup_plugin_1.detectInstalledMpvPlugin)({ - platform: 'win32', - homeDir: os.homedir(), - appDataDir: electron_1.app.getPath('appData'), - mpvExecutablePath, - }); -} -function logInstalledMpvPluginDetected(detection) { - if (!detection.path) - return; - logger.warn(`SubMiner detected an installed mpv plugin at ${detection.path}. This mpv session will use the installed plugin. Remove it to use the bundled runtime plugin automatically. Detected plugin version: ${detection.version ?? 'unknown or legacy'}.`); -} -async function promptForLegacyMpvPluginRemovalBeforeWindowsLaunch(mpvPath, detection) { - const response = await electron_1.dialog.showMessageBox({ - type: 'warning', - title: 'SubMiner mpv plugin detected', - message: [ - 'SubMiner detected an installed mpv plugin at:', - detection.path ?? 'unknown path', - '', - "This mpv session will use the installed plugin unless it is removed. Remove it now to use SubMiner's bundled runtime plugin automatically.", - `Detected plugin version: ${detection.version ?? 'unknown or legacy'}`, - ].join('\n'), - detail: 'Remove the legacy SubMiner mpv plugin files from mpv before launching this video? This moves the files to the OS trash.', - buttons: ['Remove legacy plugin', 'Continue with installed plugin', 'Cancel'], - defaultId: 0, - cancelId: 2, - }); - if (response.response === 2) { - return 'cancel'; - } - if (response.response === 1) { - return 'continue'; - } - const result = await (0, first_run_setup_plugin_1.removeLegacyMpvPluginCandidates)({ - candidates: (0, first_run_setup_plugin_1.detectInstalledFirstRunPluginCandidates)({ - platform: 'win32', - homeDir: os.homedir(), - appDataDir: electron_1.app.getPath('appData'), - mpvExecutablePath: mpvPath, - }), - trashItem: (candidatePath) => electron_1.shell.trashItem(candidatePath), - }); - if (result.ok) { - await electron_1.dialog.showMessageBox({ - type: 'info', - title: 'Legacy mpv plugin removed', - message: 'Legacy mpv plugin removed. SubMiner-managed playback will use the bundled runtime plugin.', - }); - return 'removed'; - } - await electron_1.dialog.showMessageBox({ - type: 'error', - title: 'Could not remove legacy mpv plugin', - message: 'Some legacy SubMiner mpv plugin files could not be moved to the trash.', - detail: result.failedPaths.map((failure) => `${failure.path}: ${failure.message}`).join('\n'), - }); - return 'cancel'; -} -const youtubePlaybackRuntime = (0, youtube_playback_runtime_1.createYoutubePlaybackRuntime)({ - platform: process.platform, - directPlaybackFormat: YOUTUBE_DIRECT_PLAYBACK_FORMAT, - mpvYtdlFormat: YOUTUBE_MPV_YTDL_FORMAT, - autoLaunchTimeoutMs: YOUTUBE_MPV_AUTO_LAUNCH_TIMEOUT_MS, - connectTimeoutMs: YOUTUBE_MPV_CONNECT_TIMEOUT_MS, - getSocketPath: () => appState.mpvSocketPath, - getMpvConnected: () => Boolean(appState.mpvClient?.connected), - invalidatePendingAutoplayReadyFallbacks: () => autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks(), - setAppOwnedFlowInFlight: (next) => { - youtubePrimarySubtitleNotificationRuntime.setAppOwnedFlowInFlight(next); - }, - ensureYoutubePlaybackRuntimeReady: async () => { - await ensureYoutubePlaybackRuntimeReady(); - }, - resolveYoutubePlaybackUrl: (url, format) => (0, playback_resolve_1.resolveYoutubePlaybackUrl)(url, format), - launchWindowsMpv: (playbackUrl, args) => (0, windows_mpv_launch_1.launchWindowsMpv)([playbackUrl], (0, windows_mpv_launch_1.createWindowsMpvLaunchDeps)({ - showError: (title, content) => electron_1.dialog.showErrorBox(title, content), - }), [...args, `--log-file=${DEFAULT_MPV_LOG_PATH}`], process.execPath, resolveBundledMpvRuntimePluginEntrypoint(), getResolvedConfig().mpv.executablePath, getResolvedConfig().mpv.launchMode, { - detectInstalledMpvPlugin: detectWindowsInstalledMpvPlugin, - notifyInstalledPluginDetected: logInstalledMpvPluginDetected, - resolveInstalledPluginBeforeLaunch: (detection, mpvPath) => promptForLegacyMpvPluginRemovalBeforeWindowsLaunch(mpvPath, detection), - }, { - socketPath: appState.mpvSocketPath, - binaryPath: getResolvedConfig().mpv.subminerBinaryPath, - backend: getResolvedConfig().mpv.backend, - autoStart: getResolvedConfig().mpv.autoStartSubMiner, - autoStartVisibleOverlay: getResolvedConfig().auto_start_overlay, - autoStartPauseUntilReady: getResolvedConfig().mpv.pauseUntilOverlayReady, - texthookerEnabled: getResolvedConfig().texthooker.launchAtStartup, - aniskipEnabled: getResolvedConfig().mpv.aniskipEnabled, - aniskipButtonKey: getResolvedConfig().mpv.aniskipButtonKey, - }), - waitForYoutubeMpvConnected: (timeoutMs) => waitForYoutubeMpvConnected(timeoutMs), - prepareYoutubePlaybackInMpv: (request) => prepareYoutubePlaybackInMpv(request), - runYoutubePlaybackFlow: (request) => youtubeFlowRuntime.runYoutubePlaybackFlow(request), - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), - schedule: (callback, delayMs) => setTimeout(callback, delayMs), - clearScheduled: (timer) => clearTimeout(timer), -}); -let firstRunSetupMessage = null; -const resolveWindowsMpvShortcutRuntimePaths = () => (0, windows_mpv_shortcuts_1.resolveWindowsMpvShortcutPaths)({ - appDataDir: electron_1.app.getPath('appData'), - desktopDir: electron_1.app.getPath('desktop'), -}); -const createCommandLineLauncherRuntimeOptions = () => ({ - platform: process.platform, - env: process.env, - homeDir: os.homedir(), - localAppData: process.env.LOCALAPPDATA, - userProfile: process.env.USERPROFILE, - cwd: process.cwd(), - resourcesPath: process.resourcesPath, - appExePath: process.execPath, -}); -const firstRunSetupService = (0, first_run_setup_service_1.createFirstRunSetupService)({ - platform: process.platform, - configDir: CONFIG_DIR, - getYomitanDictionaryCount: async () => { - await ensureYomitanExtensionLoaded(); - const dictionaries = await (0, services_1.getYomitanDictionaryInfo)(getYomitanParserRuntimeDeps(), { - error: (message, ...args) => logger.error(message, ...args), - info: (message, ...args) => logger.info(message, ...args), - }); - return dictionaries.length; - }, - isExternalYomitanConfigured: () => getResolvedConfig().yomitan.externalProfilePath.trim().length > 0, - detectPluginInstalled: () => { - const candidates = (0, first_run_setup_plugin_1.detectInstalledFirstRunPluginCandidates)({ - platform: process.platform, - homeDir: os.homedir(), - xdgConfigHome: process.env.XDG_CONFIG_HOME, - appDataDir: electron_1.app.getPath('appData'), - mpvExecutablePath: getResolvedConfig().mpv.executablePath, - }); - if (candidates.length > 0) { - return true; - } - const installPaths = (0, setup_state_1.resolveDefaultMpvInstallPaths)(process.platform, os.homedir(), process.env.XDG_CONFIG_HOME); - return (0, first_run_setup_plugin_1.detectInstalledFirstRunPlugin)(installPaths); - }, - detectLegacyMpvPluginCandidates: () => (0, first_run_setup_plugin_1.detectInstalledFirstRunPluginCandidates)({ - platform: process.platform, - homeDir: os.homedir(), - xdgConfigHome: process.env.XDG_CONFIG_HOME, - appDataDir: electron_1.app.getPath('appData'), - mpvExecutablePath: getResolvedConfig().mpv.executablePath, - }), - removeLegacyMpvPlugins: (candidates) => (0, first_run_setup_plugin_1.removeLegacyMpvPluginCandidates)({ - candidates, - trashItem: (candidatePath) => electron_1.shell.trashItem(candidatePath), - }), - detectWindowsMpvShortcuts: () => { - if (process.platform !== 'win32') { - return { - startMenuInstalled: false, - desktopInstalled: false, - }; - } - return (0, windows_mpv_shortcuts_1.detectWindowsMpvShortcuts)(resolveWindowsMpvShortcutRuntimePaths()); - }, - applyWindowsMpvShortcuts: async (preferences) => { - if (process.platform !== 'win32') { - return { - ok: true, - status: 'unknown', - message: '', - }; - } - return (0, windows_mpv_shortcuts_1.applyWindowsMpvShortcuts)({ - preferences, - paths: resolveWindowsMpvShortcutRuntimePaths(), - exePath: process.execPath, - writeShortcutLink: (shortcutPath, operation, details) => electron_1.shell.writeShortcutLink(shortcutPath, operation, details), - }); - }, - detectCommandLineLauncher: () => (0, command_line_launcher_1.detectCommandLineLauncher)(createCommandLineLauncherRuntimeOptions()), - installBun: async () => { - const snapshot = await (0, command_line_launcher_1.installBun)(createCommandLineLauncherRuntimeOptions()); - return { - ok: snapshot.status === 'ready', - message: snapshot.message ?? - (snapshot.status === 'ready' - ? 'Bun is ready. Open a new terminal.' - : 'Bun installation failed.'), - }; - }, - installCommandLineLauncher: async () => { - const snapshot = await (0, command_line_launcher_1.installLauncher)(createCommandLineLauncherRuntimeOptions()); - const ok = snapshot.status === 'ready' || snapshot.status === 'installed_bun_missing'; - return { - ok, - installPath: snapshot.installPath, - message: snapshot.message ?? - (ok - ? 'Command-line launcher installed. Open a new terminal.' - : 'Command-line launcher installation failed.'), - }; - }, - onStateChanged: (state) => { - appState.firstRunSetupCompleted = state.status === 'completed'; - if (appTray) { - ensureTray(); - } - }, -}); -const discordPresenceSessionStartedAtMs = Date.now(); -let discordPresenceMediaDurationSec = null; -const discordPresenceRuntime = (0, discord_presence_runtime_1.createDiscordPresenceRuntime)({ - getDiscordPresenceService: () => appState.discordPresenceService, - isDiscordPresenceEnabled: () => getResolvedConfig().discordPresence.enabled === true, - getMpvClient: () => appState.mpvClient, - getCurrentMediaTitle: () => appState.currentMediaTitle, - getCurrentMediaPath: () => appState.currentMediaPath, - getCurrentSubtitleText: () => appState.currentSubText, - getPlaybackPaused: () => appState.playbackPaused, - getFallbackMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec, - getSessionStartedAtMs: () => discordPresenceSessionStartedAtMs, - getMediaDurationSec: () => discordPresenceMediaDurationSec, - setMediaDurationSec: (next) => { - discordPresenceMediaDurationSec = next; - }, -}); -async function initializeDiscordPresenceService() { - if (getResolvedConfig().discordPresence.enabled !== true) { - appState.discordPresenceService = null; - return; - } - appState.discordPresenceService = (0, services_1.createDiscordPresenceService)({ - config: getResolvedConfig().discordPresence, - createClient: () => (0, discord_rpc_client_js_1.createDiscordRpcClient)(DISCORD_PRESENCE_APP_ID), - logDebug: (message, meta) => logger.debug(message, meta), - }); - await appState.discordPresenceService.start(); - discordPresenceRuntime.publishDiscordPresence(); -} -const ensureOverlayMpvSubtitlesHidden = (0, overlay_mpv_sub_visibility_1.createEnsureOverlayMpvSubtitlesHiddenHandler)({ - getMpvClient: () => appState.mpvClient, - getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility, - setSavedSubVisibility: (visible) => { - appState.overlaySavedMpvSubVisibility = visible; - }, - getRevision: () => appState.overlayMpvSubVisibilityRevision, - setRevision: (revision) => { - appState.overlayMpvSubVisibilityRevision = revision; - }, - setMpvSubVisibility: (visible) => { - (0, services_1.setMpvSubVisibilityRuntime)(appState.mpvClient, visible); - }, - logWarn: (message, error) => { - logger.warn(message, error); - }, -}); -const restoreOverlayMpvSubtitles = (0, overlay_mpv_sub_visibility_1.createRestoreOverlayMpvSubtitlesHandler)({ - getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility, - setSavedSubVisibility: (visible) => { - appState.overlaySavedMpvSubVisibility = visible; - }, - getRevision: () => appState.overlayMpvSubVisibilityRevision, - setRevision: (revision) => { - appState.overlayMpvSubVisibilityRevision = revision; - }, - isMpvConnected: () => Boolean(appState.mpvClient?.connected), - shouldKeepSuppressedFromVisibleOverlayBinding: () => shouldSuppressMpvSubtitlesForOverlay(), - setMpvSubVisibility: (visible) => { - (0, services_1.setMpvSubVisibilityRuntime)(appState.mpvClient, visible); - }, -}); -function shouldSuppressMpvSubtitlesForOverlay() { - return overlayManager.getVisibleOverlayVisible(); -} -function syncOverlayMpvSubtitleSuppression() { - if (shouldSuppressMpvSubtitlesForOverlay()) { - void ensureOverlayMpvSubtitlesHidden(); - return; - } - restoreOverlayMpvSubtitles(); -} -const buildImmersionMediaRuntimeMainDepsHandler = (0, startup_1.createBuildImmersionMediaRuntimeMainDepsHandler)({ - getResolvedConfig: () => getResolvedConfig(), - defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH, - getTracker: () => appState.immersionTracker, - getMpvClient: () => appState.mpvClient, - getCurrentMediaPath: () => appState.currentMediaPath, - getCurrentMediaTitle: () => appState.currentMediaTitle, - logDebug: (message) => logger.debug(message), - logInfo: (message) => logger.info(message), -}); -const buildAnilistStateRuntimeMainDepsHandler = (0, startup_1.createBuildAnilistStateRuntimeMainDepsHandler)({ - getClientSecretState: () => appState.anilistClientSecretState, - setClientSecretState: (next) => { - appState.anilistClientSecretState = (0, state_1.transitionAnilistClientSecretState)(appState.anilistClientSecretState, next); - }, - getRetryQueueState: () => appState.anilistRetryQueueState, - setRetryQueueState: (next) => { - appState.anilistRetryQueueState = (0, state_1.transitionAnilistRetryQueueState)(appState.anilistRetryQueueState, next); - }, - getUpdateQueueSnapshot: () => anilistUpdateQueue.getSnapshot(), - clearStoredToken: () => anilistTokenStore.clearToken(), - clearCachedAccessToken: () => { - anilistCachedAccessToken = null; - }, -}); -const buildConfigDerivedRuntimeMainDepsHandler = (0, startup_1.createBuildConfigDerivedRuntimeMainDepsHandler)({ - getResolvedConfig: () => getResolvedConfig(), - getRuntimeOptionsManager: () => appState.runtimeOptionsManager, - defaultJimakuLanguagePreference: config_2.DEFAULT_CONFIG.jimaku.languagePreference, - defaultJimakuMaxEntryResults: config_2.DEFAULT_CONFIG.jimaku.maxEntryResults, - defaultJimakuApiBaseUrl: config_2.DEFAULT_CONFIG.jimaku.apiBaseUrl, -}); -const buildMainSubsyncRuntimeMainDepsHandler = (0, startup_1.createBuildMainSubsyncRuntimeMainDepsHandler)({ - getMpvClient: () => appState.mpvClient, - getResolvedConfig: () => getResolvedConfig(), - getSubsyncInProgress: () => appState.subsyncInProgress, - setSubsyncInProgress: (inProgress) => { - appState.subsyncInProgress = inProgress; - }, - showMpvOsd: (text) => showMpvOsd(text), - openManualPicker: (payload) => { - openOverlayHostedModalWithOsd((deps) => (0, subsync_open_1.openSubsyncManualModal)(deps, payload), 'Subsync overlay unavailable.', 'Failed to open subsync overlay.'); - }, -}); -const immersionMediaRuntime = (0, startup_1.createImmersionMediaRuntime)(buildImmersionMediaRuntimeMainDepsHandler()); -const anilistRateLimiter = (0, rate_limiter_1.createAnilistRateLimiter)(); -const statsCoverArtFetcher = (0, cover_art_fetcher_1.createCoverArtFetcher)(anilistRateLimiter, (0, logger_1.createLogger)('main:stats-cover-art')); -const anilistStateRuntime = (0, anilist_1.createAnilistStateRuntime)(buildAnilistStateRuntimeMainDepsHandler()); -const configDerivedRuntime = (0, startup_1.createConfigDerivedRuntime)(buildConfigDerivedRuntimeMainDepsHandler()); -const subsyncRuntime = (0, startup_1.createMainSubsyncRuntime)(buildMainSubsyncRuntimeMainDepsHandler()); -const currentMediaTokenizationGate = (0, current_media_tokenization_gate_1.createCurrentMediaTokenizationGate)(); -const startupOsdSequencer = (0, startup_osd_sequencer_1.createStartupOsdSequencer)({ - showOsd: (message) => showMpvOsd(message), -}); -const youtubePrimarySubtitleNotificationRuntime = (0, youtube_primary_subtitle_notification_1.createYoutubePrimarySubtitleNotificationRuntime)({ - getPrimarySubtitleLanguages: () => getResolvedConfig().youtube.primarySubLanguages, - notifyFailure: (message) => reportYoutubeSubtitleFailure(message), - schedule: (fn, delayMs) => setTimeout(fn, delayMs), - clearSchedule: youtube_primary_subtitle_notification_1.clearYoutubePrimarySubtitleNotificationTimer, -}); -function isYoutubePlaybackActiveNow() { - return (0, youtube_playback_1.isYoutubePlaybackActive)(appState.currentMediaPath, appState.mpvClient?.currentVideoPath ?? null); -} -function reportYoutubeSubtitleFailure(message) { - const type = getResolvedConfig().ankiConnect.behavior.notificationType; - if (type === 'osd' || type === 'both') { - showMpvOsd(message); - } - if (type === 'system' || type === 'both') { - try { - (0, utils_2.showDesktopNotification)('SubMiner', { body: message }); - } - catch { - logger.warn(`Unable to show desktop notification: ${message}`); - } - } -} -async function openYoutubeTrackPickerFromPlayback() { - if (youtubeFlowRuntime.hasActiveSession()) { - showMpvOsd('YouTube subtitle flow already in progress.'); - return; - } - const currentMediaPath = appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || ''; - if (!isYoutubePlaybackActiveNow() || !currentMediaPath) { - showMpvOsd('YouTube subtitle picker is only available during YouTube playback.'); - return; - } - await youtubeFlowRuntime.openManualPicker({ - url: currentMediaPath, - }); -} -let appTray = null; -let tokenizeSubtitleDeferred = null; -function withCurrentSubtitleTiming(payload) { - return { - ...payload, - startTime: appState.mpvClient?.currentSubStart ?? null, - endTime: appState.mpvClient?.currentSubEnd ?? null, - }; -} -function emitSubtitlePayload(payload) { - const timedPayload = withCurrentSubtitleTiming(payload); - appState.currentSubtitleData = timedPayload; - broadcastToOverlayWindows('subtitle:set', timedPayload); - subtitleWsService.broadcast(timedPayload, { - enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, - mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, - }); - annotationSubtitleWsService.broadcast(timedPayload, { - enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, - mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, - }); - subtitlePrefetchService?.resume(); -} -const buildSubtitleProcessingControllerMainDepsHandler = (0, startup_1.createBuildSubtitleProcessingControllerMainDepsHandler)({ - tokenizeSubtitle: async (text) => tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : { text, tokens: null }, - emitSubtitle: (payload) => emitSubtitlePayload(payload), - logDebug: (message) => { - logger.debug(`[subtitle-processing] ${message}`); - }, - now: () => Date.now(), -}); -const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler(); -const subtitleProcessingController = (0, services_1.createSubtitleProcessingController)(subtitleProcessingControllerMainDeps); -let subtitlePrefetchService = null; -let subtitlePrefetchRefreshTimer = null; -let lastObservedTimePos = 0; -let cancelLinuxMpvFullscreenOverlayRefreshBurst = null; -const SEEK_THRESHOLD_SECONDS = 3; -function clearScheduledSubtitlePrefetchRefresh() { - if (subtitlePrefetchRefreshTimer) { - clearTimeout(subtitlePrefetchRefreshTimer); - subtitlePrefetchRefreshTimer = null; - } -} -function cancelPendingLinuxMpvFullscreenOverlayRefreshBurst() { - cancelLinuxMpvFullscreenOverlayRefreshBurst?.(); - cancelLinuxMpvFullscreenOverlayRefreshBurst = null; -} -const subtitlePrefetchInitController = (0, subtitle_prefetch_init_1.createSubtitlePrefetchInitController)({ - getCurrentService: () => subtitlePrefetchService, - setCurrentService: (service) => { - subtitlePrefetchService = service; - }, - loadSubtitleSourceText, - parseSubtitleCues: (content, filename) => (0, subtitle_cue_parser_1.parseSubtitleCues)(content, filename), - createSubtitlePrefetchService: (deps) => (0, subtitle_prefetch_1.createSubtitlePrefetchService)(deps), - tokenizeSubtitle: async (text) => tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null, - preCacheTokenization: (text, data) => { - subtitleProcessingController.preCacheTokenization(text, data); - }, - isCacheFull: () => subtitleProcessingController.isCacheFull(), - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), - onParsedSubtitleCuesChanged: (cues, sourceKey) => { - appState.activeParsedSubtitleCues = cues ?? []; - appState.activeParsedSubtitleSource = sourceKey; - }, -}); -const resolveActiveSubtitleSidebarSourceHandler = (0, subtitle_prefetch_runtime_1.createResolveActiveSubtitleSidebarSourceHandler)({ - getFfmpegPath: () => getResolvedConfig().subsync.ffmpeg_path.trim() || 'ffmpeg', - extractInternalSubtitleTrack: (ffmpegPath, videoPath, track) => extractInternalSubtitleTrackToTempFile(ffmpegPath, videoPath, track), -}); -async function refreshSubtitleSidebarFromSource(sourcePath) { - const normalizedSourcePath = (0, subtitle_prefetch_source_1.resolveSubtitleSourcePath)(sourcePath.trim()); - if (!normalizedSourcePath) { - return; - } - await subtitlePrefetchInitController.initSubtitlePrefetch(normalizedSourcePath, lastObservedTimePos, normalizedSourcePath); -} -const refreshSubtitlePrefetchFromActiveTrackHandler = (0, subtitle_prefetch_runtime_1.createRefreshSubtitlePrefetchFromActiveTrackHandler)({ - getMpvClient: () => appState.mpvClient, - getLastObservedTimePos: () => lastObservedTimePos, - subtitlePrefetchInitController, - resolveActiveSubtitleSidebarSource: (input) => resolveActiveSubtitleSidebarSourceHandler(input), -}); -function scheduleSubtitlePrefetchRefresh(delayMs = 0) { - clearScheduledSubtitlePrefetchRefresh(); - subtitlePrefetchRefreshTimer = setTimeout(() => { - subtitlePrefetchRefreshTimer = null; - void refreshSubtitlePrefetchFromActiveTrackHandler(); - }, delayMs); -} -const subtitlePrefetchRuntime = { - cancelPendingInit: () => subtitlePrefetchInitController.cancelPendingInit(), - initSubtitlePrefetch: subtitlePrefetchInitController.initSubtitlePrefetch, - refreshSubtitleSidebarFromSource: (sourcePath) => refreshSubtitleSidebarFromSource(sourcePath), - refreshSubtitlePrefetchFromActiveTrack: () => refreshSubtitlePrefetchFromActiveTrackHandler(), - scheduleSubtitlePrefetchRefresh: (delayMs) => scheduleSubtitlePrefetchRefresh(delayMs), - clearScheduledSubtitlePrefetchRefresh: () => clearScheduledSubtitlePrefetchRefresh(), -}; -const overlayShortcutsRuntime = (0, overlay_shortcuts_runtime_1.createOverlayShortcutsRuntimeService)((0, shortcuts_1.createBuildOverlayShortcutsRuntimeMainDepsHandler)({ - getConfiguredShortcuts: () => getConfiguredShortcuts(), - getShortcutsRegistered: () => appState.shortcutsRegistered, - setShortcutsRegistered: (registered) => { - appState.shortcutsRegistered = registered; - }, - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - isOverlayShortcutContextActive: () => { - if (process.platform !== 'win32') { - return true; - } - if (!overlayManager.getVisibleOverlayVisible()) { - return false; - } - const windowTracker = appState.windowTracker; - if (!windowTracker || !windowTracker.isTracking()) { - return false; - } - return windowTracker.isTargetWindowFocused(); - }, - showMpvOsd: (text) => showMpvOsd(text), - openRuntimeOptionsPalette: () => { - openRuntimeOptionsPalette(); - }, - openCharacterDictionary: () => { - openCharacterDictionaryOverlay(); - }, - openJimaku: () => { - openJimakuOverlay(); - }, - markAudioCard: () => markLastCardAsAudioCard(), - copySubtitleMultiple: (timeoutMs) => { - startPendingMultiCopy(timeoutMs); - }, - copySubtitle: () => { - copyCurrentSubtitle(); - }, - toggleSecondarySubMode: () => handleCycleSecondarySubMode(), - updateLastCardFromClipboard: () => updateLastCardFromClipboard(), - triggerFieldGrouping: () => triggerFieldGrouping(), - triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), - mineSentenceCard: () => mineSentenceCard(), - mineSentenceMultiple: (timeoutMs) => { - startPendingMineSentenceMultiple(timeoutMs); - }, - cancelPendingMultiCopy: () => { - cancelPendingMultiCopy(); - }, - cancelPendingMineSentenceMultiple: () => { - cancelPendingMineSentenceMultiple(); - }, -})()); -syncOverlayShortcutsForModal = (isActive) => { - if (isActive) { - overlayShortcutsRuntime.unregisterOverlayShortcuts(); - } - else { - overlayShortcutsRuntime.syncOverlayShortcuts(); - } -}; -const buildConfigHotReloadMessageMainDepsHandler = (0, overlay_1.createBuildConfigHotReloadMessageMainDepsHandler)({ - showMpvOsd: (message) => showMpvOsd(message), - showDesktopNotification: (title, options) => (0, utils_2.showDesktopNotification)(title, options), -}); -const configHotReloadMessageMainDeps = buildConfigHotReloadMessageMainDepsHandler(); -const notifyConfigHotReloadMessage = (0, overlay_1.createConfigHotReloadMessageHandler)(configHotReloadMessageMainDeps); -const buildWatchConfigPathMainDepsHandler = (0, overlay_1.createBuildWatchConfigPathMainDepsHandler)({ - fileExists: (targetPath) => fs.existsSync(targetPath), - dirname: (targetPath) => path.dirname(targetPath), - watchPath: (targetPath, listener) => fs.watch(targetPath, listener), -}); -const watchConfigPathHandler = (0, overlay_1.createWatchConfigPathHandler)(buildWatchConfigPathMainDepsHandler()); -const buildConfigHotReloadAppliedMainDepsHandler = (0, overlay_1.createBuildConfigHotReloadAppliedMainDepsHandler)({ - setKeybindings: (keybindings) => { - appState.keybindings = keybindings; - }, - setSessionBindings: (sessionBindings, sessionBindingWarnings) => { - persistSessionBindings(sessionBindings, sessionBindingWarnings); - }, - refreshGlobalAndOverlayShortcuts: () => { - refreshGlobalAndOverlayShortcuts(); - }, - setSecondarySubMode: (mode) => { - setSecondarySubMode(mode); - }, - broadcastToOverlayWindows: (channel, payload) => { - broadcastToOverlayWindows(channel, payload); - }, - applyAnkiRuntimeConfigPatch: (patch) => { - if (appState.ankiIntegration) { - appState.ankiIntegration.applyRuntimeConfigPatch(patch); - } - }, -}); -const applyConfigHotReloadDiff = (0, overlay_1.createConfigHotReloadAppliedHandler)(buildConfigHotReloadAppliedMainDepsHandler()); -const buildConfigHotReloadRuntimeMainDepsHandler = (0, overlay_1.createBuildConfigHotReloadRuntimeMainDepsHandler)({ - getCurrentConfig: () => getResolvedConfig(), - reloadConfigStrict: () => configService.reloadConfigStrict(), - watchConfigPath: (configPath, onChange) => watchConfigPathHandler(configPath, onChange), - setTimeout: (callback, delayMs) => setTimeout(callback, delayMs), - clearTimeout: (timeout) => clearTimeout(timeout), - debounceMs: 250, - onHotReloadApplied: applyConfigHotReloadDiff, - onRestartRequired: (fields) => notifyConfigHotReloadMessage((0, overlay_1.buildRestartRequiredConfigMessage)(fields)), - onInvalidConfig: notifyConfigHotReloadMessage, - onValidationWarnings: (configPath, warnings) => { - (0, utils_2.showDesktopNotification)('SubMiner', { - body: (0, config_validation_1.buildConfigWarningNotificationBody)(configPath, warnings), - }); - if (process.platform === 'darwin') { - electron_1.dialog.showErrorBox('SubMiner config validation warning', (0, config_validation_1.buildConfigWarningDialogDetails)(configPath, warnings)); - } - }, -}); -const configHotReloadRuntime = (0, services_1.createConfigHotReloadRuntime)(buildConfigHotReloadRuntimeMainDepsHandler()); -const configSettingsRuntime = (0, config_settings_runtime_1.createConfigSettingsRuntime)({ - fields: configSettingsFields, - getConfigPath: () => configService.getConfigPath(), - getRawConfig: () => configService.getRawConfig(), - getConfig: () => configService.getConfig(), - getWarnings: () => configService.getWarnings(), - reloadConfigStrict: () => configService.reloadConfigStrict(), - defaultAnkiConnectUrl: config_2.DEFAULT_CONFIG.ankiConnect.url, - createAnkiClient: (url) => new anki_connect_1.AnkiConnectClient(url), - getSettingsWindow: () => appState.configSettingsWindow, - setSettingsWindow: (window) => { - appState.configSettingsWindow = window; - }, - createSettingsWindow: (0, setup_window_factory_1.createCreateConfigSettingsWindowHandler)({ - createBrowserWindow: (options) => new electron_1.BrowserWindow(options), - preloadPath: path.join(__dirname, 'preload-settings.js'), - }), - settingsHtmlPath: path.join(__dirname, 'settings', 'index.html'), - openPath: (targetPath) => electron_1.shell.openPath(targetPath), - ipcMain: electron_1.ipcMain, - ipcChannels: contracts_1.IPC_CHANNELS.request, - log: (message) => logger.error(message), -}); -configSettingsRuntime.registerHandlers(); -const openConfigSettingsWindow = () => configSettingsRuntime.openWindow(); -const buildDictionaryRootsHandler = (0, startup_1.createBuildDictionaryRootsMainHandler)({ - platform: process.platform, - dirname: __dirname, - appPath: electron_1.app.getAppPath(), - resourcesPath: process.resourcesPath, - userDataPath: USER_DATA_PATH, - appUserDataPath: electron_1.app.getPath('userData'), - homeDir: os.homedir(), - appDataDir: process.env.APPDATA, - cwd: process.cwd(), - joinPath: (...parts) => path.join(...parts), -}); -const buildFrequencyDictionaryRootsHandler = (0, startup_1.createBuildFrequencyDictionaryRootsMainHandler)({ - platform: process.platform, - dirname: __dirname, - appPath: electron_1.app.getAppPath(), - resourcesPath: process.resourcesPath, - userDataPath: USER_DATA_PATH, - appUserDataPath: electron_1.app.getPath('userData'), - homeDir: os.homedir(), - appDataDir: process.env.APPDATA, - cwd: process.cwd(), - joinPath: (...parts) => path.join(...parts), -}); -const jlptDictionaryRuntime = (0, jlpt_runtime_1.createJlptDictionaryRuntimeService)((0, startup_1.createBuildJlptDictionaryRuntimeMainDepsHandler)({ - isJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt, - getDictionaryRoots: () => buildDictionaryRootsHandler(), - getJlptDictionarySearchPaths: jlpt_runtime_1.getJlptDictionarySearchPaths, - setJlptLevelLookup: (lookup) => { - appState.jlptLevelLookup = lookup; - }, - logInfo: (message) => logger.info(message), -})()); -const frequencyDictionaryRuntime = (0, frequency_dictionary_runtime_1.createFrequencyDictionaryRuntimeService)((0, startup_1.createBuildFrequencyDictionaryRuntimeMainDepsHandler)({ - isFrequencyDictionaryEnabled: () => getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - getDictionaryRoots: () => buildFrequencyDictionaryRootsHandler(), - getFrequencyDictionarySearchPaths: frequency_dictionary_runtime_1.getFrequencyDictionarySearchPaths, - getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath, - setFrequencyRankLookup: (lookup) => { - appState.frequencyRankLookup = lookup; - }, - logInfo: (message) => logger.info(message), -})()); -const buildGetFieldGroupingResolverMainDepsHandler = (0, overlay_1.createBuildGetFieldGroupingResolverMainDepsHandler)({ - getResolver: () => appState.fieldGroupingResolver, -}); -const getFieldGroupingResolverMainDeps = buildGetFieldGroupingResolverMainDepsHandler(); -const getFieldGroupingResolverHandler = (0, overlay_1.createGetFieldGroupingResolverHandler)(getFieldGroupingResolverMainDeps); -function getFieldGroupingResolver() { - return getFieldGroupingResolverHandler(); -} -const buildSetFieldGroupingResolverMainDepsHandler = (0, overlay_1.createBuildSetFieldGroupingResolverMainDepsHandler)({ - setResolver: (resolver) => { - appState.fieldGroupingResolver = resolver; - }, - nextSequence: () => { - appState.fieldGroupingResolverSequence += 1; - return appState.fieldGroupingResolverSequence; - }, - getSequence: () => appState.fieldGroupingResolverSequence, -}); -const setFieldGroupingResolverMainDeps = buildSetFieldGroupingResolverMainDepsHandler(); -const setFieldGroupingResolverHandler = (0, overlay_1.createSetFieldGroupingResolverHandler)(setFieldGroupingResolverMainDeps); -function setFieldGroupingResolver(resolver) { - setFieldGroupingResolverHandler(resolver); -} -const fieldGroupingOverlayRuntime = (0, services_1.createFieldGroupingOverlayRuntime)((0, overlay_1.createBuildFieldGroupingOverlayMainDepsHandler)({ - getMainWindow: () => overlayManager.getMainWindow(), - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - getResolver: () => getFieldGroupingResolver(), - setResolver: (resolver) => setFieldGroupingResolver(resolver), - getRestoreVisibleOverlayOnModalClose: () => overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(), - sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions), -})()); -const createFieldGroupingCallback = fieldGroupingOverlayRuntime.createFieldGroupingCallback; -const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, 'subtitle-positions'); -const mediaRuntime = (0, media_runtime_1.createMediaRuntimeService)((0, startup_1.createBuildMediaRuntimeMainDepsHandler)({ - isRemoteMediaPath: (mediaPath) => (0, utils_1.isRemoteMediaPath)(mediaPath), - loadSubtitlePosition: () => loadSubtitlePosition(), - getCurrentMediaPath: () => appState.currentMediaPath, - getPendingSubtitlePosition: () => appState.pendingSubtitlePosition, - getSubtitlePositionsDir: () => SUBTITLE_POSITIONS_DIR, - setCurrentMediaPath: (nextPath) => { - appState.currentMediaPath = nextPath; - }, - clearPendingSubtitlePosition: () => { - appState.pendingSubtitlePosition = null; - }, - setSubtitlePosition: (position) => { - appState.subtitlePosition = position; - }, - broadcastToOverlayWindows: (channel, payload) => { - broadcastToOverlayWindows(channel, payload); - }, - getCurrentMediaTitle: () => appState.currentMediaTitle, - setCurrentMediaTitle: (title) => { - appState.currentMediaTitle = title; - }, -})()); -const characterDictionaryRuntime = (0, character_dictionary_runtime_1.createCharacterDictionaryRuntimeService)({ - userDataPath: USER_DATA_PATH, - getCurrentMediaPath: () => appState.currentMediaPath, - getCurrentMediaTitle: () => appState.currentMediaTitle, - resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath), - guessAnilistMediaInfo: (mediaPath, mediaTitle) => (0, anilist_updater_1.guessAnilistMediaInfo)(mediaPath, mediaTitle), - getCollapsibleSectionOpenState: (section) => getResolvedConfig().anilist.characterDictionary.collapsibleSections[section], - now: () => Date.now(), - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), -}); -const characterDictionaryAutoSyncRuntime = (0, character_dictionary_auto_sync_1.createCharacterDictionaryAutoSyncRuntimeService)({ - userDataPath: USER_DATA_PATH, - getConfig: () => { - const config = getResolvedConfig().anilist.characterDictionary; - return { - enabled: config.enabled && - yomitanProfilePolicy.isCharacterDictionaryEnabled() && - !isYoutubePlaybackActiveNow(), - maxLoaded: config.maxLoaded, - profileScope: config.profileScope, - }; - }, - getOrCreateCurrentSnapshot: (targetPath, progress) => characterDictionaryRuntime.getOrCreateCurrentSnapshot(targetPath, progress), - buildMergedDictionary: (mediaIds) => characterDictionaryRuntime.buildMergedDictionary(mediaIds), - waitForYomitanMutationReady: () => currentMediaTokenizationGate.waitUntilReady(appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null), - getYomitanDictionaryInfo: async () => { - await ensureYomitanExtensionLoaded(); - return await (0, services_1.getYomitanDictionaryInfo)(getYomitanParserRuntimeDeps(), { - error: (message, ...args) => logger.error(message, ...args), - info: (message, ...args) => logger.info(message, ...args), - }); - }, - importYomitanDictionary: async (zipPath) => { - if (yomitanProfilePolicy.isExternalReadOnlyMode()) { - yomitanProfilePolicy.logSkippedWrite((0, yomitan_read_only_log_1.formatSkippedYomitanWriteAction)('importYomitanDictionary', zipPath)); - return false; - } - await ensureYomitanExtensionLoaded(); - return await (0, services_1.importYomitanDictionaryFromZip)(zipPath, getYomitanParserRuntimeDeps(), { - error: (message, ...args) => logger.error(message, ...args), - info: (message, ...args) => logger.info(message, ...args), - }); - }, - deleteYomitanDictionary: async (dictionaryTitle) => { - if (yomitanProfilePolicy.isExternalReadOnlyMode()) { - yomitanProfilePolicy.logSkippedWrite((0, yomitan_read_only_log_1.formatSkippedYomitanWriteAction)('deleteYomitanDictionary', dictionaryTitle)); - return false; - } - await ensureYomitanExtensionLoaded(); - return await (0, services_1.deleteYomitanDictionaryByTitle)(dictionaryTitle, getYomitanParserRuntimeDeps(), { - error: (message, ...args) => logger.error(message, ...args), - info: (message, ...args) => logger.info(message, ...args), - }); - }, - upsertYomitanDictionarySettings: async (dictionaryTitle, profileScope) => { - if (yomitanProfilePolicy.isExternalReadOnlyMode()) { - yomitanProfilePolicy.logSkippedWrite((0, yomitan_read_only_log_1.formatSkippedYomitanWriteAction)('upsertYomitanDictionarySettings', dictionaryTitle)); - return false; - } - await ensureYomitanExtensionLoaded(); - return await (0, services_1.upsertYomitanDictionarySettings)(dictionaryTitle, profileScope, getYomitanParserRuntimeDeps(), { - error: (message, ...args) => logger.error(message, ...args), - info: (message, ...args) => logger.info(message, ...args), - }); - }, - now: () => Date.now(), - schedule: (fn, delayMs) => setTimeout(fn, delayMs), - clearSchedule: (timer) => clearTimeout(timer), - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), - onSyncStatus: (event) => { - (0, character_dictionary_auto_sync_notifications_1.notifyCharacterDictionaryAutoSyncStatus)(event, { - getNotificationType: () => getResolvedConfig().ankiConnect.behavior.notificationType, - showOsd: (message) => showMpvOsd(message), - showDesktopNotification: (title, options) => (0, utils_2.showDesktopNotification)(title, options), - startupOsdSequencer, - }); - }, - onSyncComplete: ({ mediaId, mediaTitle, changed }) => { - (0, character_dictionary_auto_sync_completion_1.handleCharacterDictionaryAutoSyncComplete)({ - mediaId, - mediaTitle, - changed, - }, { - hasParserWindow: () => Boolean(appState.yomitanParserWindow), - clearParserCaches: () => { - if (appState.yomitanParserWindow) { - (0, services_1.clearYomitanParserCachesForWindow)(appState.yomitanParserWindow); - } - }, - invalidateTokenizationCache: () => { - subtitleProcessingController.invalidateTokenizationCache(); - }, - refreshSubtitlePrefetch: () => { - subtitlePrefetchService?.onSeek(lastObservedTimePos); - }, - refreshCurrentSubtitle: () => { - subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText); - }, - logInfo: (message) => logger.info(message), - }); - }, -}); -const overlayVisibilityRuntime = (0, overlay_visibility_runtime_1.createOverlayVisibilityRuntimeService)((0, overlay_1.createBuildOverlayVisibilityRuntimeMainDepsHandler)({ - getMainWindow: () => overlayManager.getMainWindow(), - getModalActive: () => overlayModalInputState.getModalInputExclusive(), - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - getForceMousePassthrough: () => appState.statsOverlayVisible, - getOverlayInteractionActive: () => visibleOverlayInteractionActive, - getWindowTracker: () => appState.windowTracker, - getLastKnownWindowsForegroundProcessName: () => lastWindowsVisibleOverlayForegroundProcessName, - getWindowsOverlayProcessName: () => path.parse(process.execPath).name.toLowerCase(), - getWindowsFocusHandoffGraceActive: () => hasWindowsVisibleOverlayFocusHandoffGrace(), - getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown, - setTrackerNotReadyWarningShown: (shown) => { - appState.trackerNotReadyWarningShown = shown; - }, - updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry), - ensureOverlayWindowLevel: (window) => { - ensureOverlayWindowLevel(window); - }, - syncWindowsOverlayToMpvZOrder: (_window) => { - requestWindowsVisibleOverlayZOrderSync(); - }, - syncPrimaryOverlayWindowLayer: (layer) => { - syncPrimaryOverlayWindowLayer(layer); - }, - enforceOverlayLayerOrder: () => { - enforceOverlayLayerOrder(); - }, - syncOverlayShortcuts: () => { - overlayShortcutsRuntime.syncOverlayShortcuts(); - }, - isMacOSPlatform: () => process.platform === 'darwin', - isWindowsPlatform: () => process.platform === 'win32', - showOverlayLoadingOsd: (message) => { - showMpvOsd(message); - }, - resolveFallbackBounds: () => { - const cursorPoint = electron_1.screen.getCursorScreenPoint(); - const display = electron_1.screen.getDisplayNearestPoint(cursorPoint); - const fallbackBounds = display.workArea; - return { - x: fallbackBounds.x, - y: fallbackBounds.y, - width: fallbackBounds.width, - height: fallbackBounds.height, - }; - }, -})()); -const VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS = [0, 25, 100, 250]; -const WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS = [0, 48, 120, 240, 480]; -const WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS = 75; -const WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS = 200; -let visibleOverlayBlurRefreshTimeouts = []; -let windowsVisibleOverlayZOrderRetryTimeouts = []; -let windowsVisibleOverlayZOrderSyncInFlight = false; -let windowsVisibleOverlayZOrderSyncQueued = false; -let windowsVisibleOverlayForegroundPollInterval = null; -let lastWindowsVisibleOverlayForegroundProcessName = null; -let lastWindowsVisibleOverlayBlurredAtMs = 0; -let visibleOverlayInteractionActive = false; -function clearVisibleOverlayBlurRefreshTimeouts() { - for (const timeout of visibleOverlayBlurRefreshTimeouts) { - clearTimeout(timeout); - } - visibleOverlayBlurRefreshTimeouts = []; -} -function clearWindowsVisibleOverlayZOrderRetryTimeouts() { - for (const timeout of windowsVisibleOverlayZOrderRetryTimeouts) { - clearTimeout(timeout); - } - windowsVisibleOverlayZOrderRetryTimeouts = []; -} -function getWindowsNativeWindowHandle(window) { - const handle = window.getNativeWindowHandle(); - return handle.length >= 8 - ? handle.readBigUInt64LE(0).toString() - : BigInt(handle.readUInt32LE(0)).toString(); -} -function getWindowsNativeWindowHandleNumber(window) { - const handle = window.getNativeWindowHandle(); - return handle.length >= 8 ? Number(handle.readBigUInt64LE(0)) : handle.readUInt32LE(0); -} -function resolveWindowsOverlayBindTargetHandle(targetMpvSocketPath) { - if (process.platform !== 'win32') { - return null; - } - try { - if (targetMpvSocketPath) { - const windowTracker = appState.windowTracker; - const trackedHandle = windowTracker?.getTargetWindowHandle?.(); - if (typeof trackedHandle === 'number' && Number.isFinite(trackedHandle)) { - return trackedHandle; - } - } - return (0, windows_helper_1.findWindowsMpvTargetWindowHandle)(); - } - catch { - return null; - } -} -async function syncWindowsVisibleOverlayToMpvZOrder() { - if (process.platform !== 'win32') { - return false; - } - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || - mainWindow.isDestroyed() || - !mainWindow.isVisible() || - !overlayManager.getVisibleOverlayVisible()) { - return false; - } - const windowTracker = appState.windowTracker; - if (!windowTracker) { - return false; - } - if (typeof windowTracker.isTargetWindowMinimized === 'function' && - windowTracker.isTargetWindowMinimized()) { - return false; - } - if (!windowTracker.isTracking() && windowTracker.getGeometry() === null) { - return false; - } - const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); - const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath); - if (targetWindowHwnd !== null && (0, windows_helper_1.bindWindowsOverlayAboveMpv)(overlayHwnd, targetWindowHwnd)) { - mainWindow.setOpacity?.(1); - return true; - } - return false; -} -function requestWindowsVisibleOverlayZOrderSync() { - if (process.platform !== 'win32') { - return; - } - if (windowsVisibleOverlayZOrderSyncInFlight) { - windowsVisibleOverlayZOrderSyncQueued = true; - return; - } - windowsVisibleOverlayZOrderSyncInFlight = true; - void syncWindowsVisibleOverlayToMpvZOrder() - .catch((error) => { - logger.warn('Failed to bind Windows overlay z-order to mpv', error); - }) - .finally(() => { - windowsVisibleOverlayZOrderSyncInFlight = false; - if (!windowsVisibleOverlayZOrderSyncQueued) { - return; - } - windowsVisibleOverlayZOrderSyncQueued = false; - requestWindowsVisibleOverlayZOrderSync(); - }); -} -function scheduleWindowsVisibleOverlayZOrderSyncBurst() { - if (process.platform !== 'win32') { - return; - } - clearWindowsVisibleOverlayZOrderRetryTimeouts(); - for (const delayMs of WINDOWS_VISIBLE_OVERLAY_Z_ORDER_RETRY_DELAYS_MS) { - const retryTimeout = setTimeout(() => { - windowsVisibleOverlayZOrderRetryTimeouts = windowsVisibleOverlayZOrderRetryTimeouts.filter((timeout) => timeout !== retryTimeout); - requestWindowsVisibleOverlayZOrderSync(); - }, delayMs); - windowsVisibleOverlayZOrderRetryTimeouts.push(retryTimeout); - } -} -function hasWindowsVisibleOverlayFocusHandoffGrace() { - return (process.platform === 'win32' && - lastWindowsVisibleOverlayBlurredAtMs > 0 && - Date.now() - lastWindowsVisibleOverlayBlurredAtMs <= - WINDOWS_VISIBLE_OVERLAY_FOCUS_HANDOFF_GRACE_MS); -} -function shouldPollWindowsVisibleOverlayForegroundProcess() { - if (process.platform !== 'win32' || !overlayManager.getVisibleOverlayVisible()) { - return false; - } - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - return false; - } - const windowTracker = appState.windowTracker; - if (!windowTracker) { - return false; - } - if (typeof windowTracker.isTargetWindowMinimized === 'function' && - windowTracker.isTargetWindowMinimized()) { - return false; - } - const overlayFocused = mainWindow.isFocused(); - const trackerFocused = windowTracker.isTargetWindowFocused?.() ?? false; - return !overlayFocused && !trackerFocused; -} -function maybePollWindowsVisibleOverlayForegroundProcess() { - if (!shouldPollWindowsVisibleOverlayForegroundProcess()) { - lastWindowsVisibleOverlayForegroundProcessName = null; - return; - } - const processName = (0, windows_helper_1.getWindowsForegroundProcessName)(); - const normalizedProcessName = processName?.trim().toLowerCase() ?? null; - const previousProcessName = lastWindowsVisibleOverlayForegroundProcessName; - lastWindowsVisibleOverlayForegroundProcessName = normalizedProcessName; - if (normalizedProcessName !== previousProcessName) { - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - } - if (normalizedProcessName === 'mpv' && previousProcessName !== 'mpv') { - requestWindowsVisibleOverlayZOrderSync(); - } -} -function ensureWindowsVisibleOverlayForegroundPollLoop() { - if (process.platform !== 'win32' || windowsVisibleOverlayForegroundPollInterval !== null) { - return; - } - windowsVisibleOverlayForegroundPollInterval = setInterval(() => { - maybePollWindowsVisibleOverlayForegroundProcess(); - }, WINDOWS_VISIBLE_OVERLAY_FOREGROUND_POLL_INTERVAL_MS); -} -function clearWindowsVisibleOverlayForegroundPollLoop() { - if (windowsVisibleOverlayForegroundPollInterval === null) { - return; - } - clearInterval(windowsVisibleOverlayForegroundPollInterval); - windowsVisibleOverlayForegroundPollInterval = null; -} -function scheduleVisibleOverlayBlurRefresh() { - if (process.platform !== 'win32' && process.platform !== 'darwin') { - return; - } - if (process.platform === 'win32') { - lastWindowsVisibleOverlayBlurredAtMs = Date.now(); - } - clearVisibleOverlayBlurRefreshTimeouts(); - for (const delayMs of VISIBLE_OVERLAY_BLUR_REFRESH_DELAYS_MS) { - const refreshTimeout = setTimeout(() => { - visibleOverlayBlurRefreshTimeouts = visibleOverlayBlurRefreshTimeouts.filter((timeout) => timeout !== refreshTimeout); - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - }, delayMs); - visibleOverlayBlurRefreshTimeouts.push(refreshTimeout); - } -} -ensureWindowsVisibleOverlayForegroundPollLoop(); -const buildGetRuntimeOptionsStateMainDepsHandler = (0, overlay_1.createBuildGetRuntimeOptionsStateMainDepsHandler)({ - getRuntimeOptionsManager: () => appState.runtimeOptionsManager, -}); -const getRuntimeOptionsStateMainDeps = buildGetRuntimeOptionsStateMainDepsHandler(); -const getRuntimeOptionsStateHandler = (0, overlay_1.createGetRuntimeOptionsStateHandler)(getRuntimeOptionsStateMainDeps); -function getRuntimeOptionsState() { - return getRuntimeOptionsStateHandler(); -} -function getOverlayWindows() { - return overlayManager.getOverlayWindows(); -} -const buildRestorePreviousSecondarySubVisibilityMainDepsHandler = (0, overlay_1.createBuildRestorePreviousSecondarySubVisibilityMainDepsHandler)({ - getMpvClient: () => appState.mpvClient, -}); -syncOverlayVisibilityForModal = () => { - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); -}; -function broadcastToOverlayWindows(channel, ...args) { - overlayManager.broadcastToOverlayWindows(channel, ...args); -} -const buildBroadcastRuntimeOptionsChangedMainDepsHandler = (0, overlay_1.createBuildBroadcastRuntimeOptionsChangedMainDepsHandler)({ - broadcastRuntimeOptionsChangedRuntime: services_1.broadcastRuntimeOptionsChangedRuntime, - getRuntimeOptionsState: () => getRuntimeOptionsState(), - broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args), -}); -const buildSendToActiveOverlayWindowMainDepsHandler = (0, overlay_1.createBuildSendToActiveOverlayWindowMainDepsHandler)({ - sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) => overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions), -}); -const buildSetOverlayDebugVisualizationEnabledMainDepsHandler = (0, overlay_1.createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler)({ - setOverlayDebugVisualizationEnabledRuntime: services_1.setOverlayDebugVisualizationEnabledRuntime, - getCurrentEnabled: () => appState.overlayDebugVisualizationEnabled, - setCurrentEnabled: (next) => { - appState.overlayDebugVisualizationEnabled = next; - }, -}); -const buildOpenRuntimeOptionsPaletteMainDepsHandler = (0, overlay_1.createBuildOpenRuntimeOptionsPaletteMainDepsHandler)({ - openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(), -}); -const overlayVisibilityComposer = (0, composers_1.composeOverlayVisibilityRuntime)({ - overlayVisibilityRuntime, - restorePreviousSecondarySubVisibilityMainDeps: buildRestorePreviousSecondarySubVisibilityMainDepsHandler(), - broadcastRuntimeOptionsChangedMainDeps: buildBroadcastRuntimeOptionsChangedMainDepsHandler(), - sendToActiveOverlayWindowMainDeps: buildSendToActiveOverlayWindowMainDepsHandler(), - setOverlayDebugVisualizationEnabledMainDeps: buildSetOverlayDebugVisualizationEnabledMainDepsHandler(), - openRuntimeOptionsPaletteMainDeps: buildOpenRuntimeOptionsPaletteMainDepsHandler(), -}); -function restorePreviousSecondarySubVisibility() { - overlayVisibilityComposer.restorePreviousSecondarySubVisibility(); -} -function broadcastRuntimeOptionsChanged() { - overlayVisibilityComposer.broadcastRuntimeOptionsChanged(); -} -function sendToActiveOverlayWindow(channel, payload, runtimeOptions) { - return overlayVisibilityComposer.sendToActiveOverlayWindow(channel, payload, runtimeOptions); -} -function setOverlayDebugVisualizationEnabled(enabled) { - overlayVisibilityComposer.setOverlayDebugVisualizationEnabled(enabled); -} -function createOverlayHostedModalOpenDeps() { - return { - ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(), - ensureOverlayWindowsReadyForVisibilityActions: () => ensureOverlayWindowsReadyForVisibilityActions(), - sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => sendToActiveOverlayWindow(channel, payload, runtimeOptions), - waitForModalOpen: (modal, timeoutMs) => overlayModalRuntime.waitForModalOpen(modal, timeoutMs), - logWarn: (message) => logger.warn(message), - }; -} -function openOverlayHostedModalWithOsd(openModal, unavailableMessage, failureLogMessage) { - void openModal(createOverlayHostedModalOpenDeps()) - .then((opened) => { - if (!opened) { - showMpvOsd(unavailableMessage); - } - }) - .catch((error) => { - logger.error(failureLogMessage, error); - showMpvOsd(unavailableMessage); - }); -} -function openRuntimeOptionsPalette() { - openOverlayHostedModalWithOsd(runtime_options_open_1.openRuntimeOptionsModal, 'Runtime options overlay unavailable.', 'Failed to open runtime options overlay.'); -} -function openJimakuOverlay() { - openOverlayHostedModalWithOsd(jimaku_open_1.openJimakuModal, 'Jimaku overlay unavailable.', 'Failed to open Jimaku overlay.'); -} -function openSessionHelpOverlay() { - openOverlayHostedModalWithOsd(session_help_open_1.openSessionHelpModal, 'Session help overlay unavailable.', 'Failed to open session help overlay.'); -} -function openCharacterDictionaryOverlay() { - openOverlayHostedModalWithOsd(character_dictionary_open_1.openCharacterDictionaryModal, 'Character dictionary overlay unavailable.', 'Failed to open character dictionary overlay.'); -} -function openControllerSelectOverlay() { - openOverlayHostedModalWithOsd(controller_select_open_1.openControllerSelectModal, 'Controller select overlay unavailable.', 'Failed to open controller select overlay.'); -} -function openControllerDebugOverlay() { - openOverlayHostedModalWithOsd(controller_debug_open_1.openControllerDebugModal, 'Controller debug overlay unavailable.', 'Failed to open controller debug overlay.'); -} -function openPlaylistBrowser() { - if (!appState.mpvClient?.connected) { - showMpvOsd('Playlist browser requires active playback.'); - return; - } - openOverlayHostedModalWithOsd(playlist_browser_open_1.openPlaylistBrowser, 'Playlist browser overlay unavailable.', 'Failed to open playlist browser overlay.'); -} -function getResolvedConfig() { - return configService.getConfig(); -} -function getRuntimeBooleanOption(id, fallback) { - const value = appState.runtimeOptionsManager?.getOptionValue(id); - return typeof value === 'boolean' ? value : fallback; -} -function shouldInitializeMecabForAnnotations() { - const config = getResolvedConfig(); - const knownWordsEnabled = getRuntimeBooleanOption('subtitle.annotation.knownWords.highlightEnabled', config.ankiConnect.knownWords.highlightEnabled); - const nPlusOneEnabled = getRuntimeBooleanOption('subtitle.annotation.nPlusOne', config.ankiConnect.nPlusOne.enabled); - const jlptEnabled = getRuntimeBooleanOption('subtitle.annotation.jlpt', config.subtitleStyle.enableJlpt); - const frequencyEnabled = getRuntimeBooleanOption('subtitle.annotation.frequency', config.subtitleStyle.frequencyDictionary.enabled); - return knownWordsEnabled || nPlusOneEnabled || jlptEnabled || frequencyEnabled; -} -const { getResolvedJellyfinConfig, reportJellyfinRemoteProgress, reportJellyfinRemoteStopped, startJellyfinRemoteSession, stopJellyfinRemoteSession, runJellyfinCommand, openJellyfinSetupWindow, getJellyfinClientInfo, } = (0, composers_1.composeJellyfinRuntimeHandlers)({ - getResolvedJellyfinConfigMainDeps: { - getResolvedConfig: () => getResolvedConfig(), - loadStoredSession: () => jellyfinTokenStore.loadSession(), - getEnv: (name) => process.env[name], - }, - getJellyfinClientInfoMainDeps: { - getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(), - getDefaultJellyfinConfig: () => config_2.DEFAULT_CONFIG.jellyfin, - }, - waitForMpvConnectedMainDeps: { - getMpvClient: () => appState.mpvClient, - now: () => Date.now(), - sleep: (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)), - }, - launchMpvIdleForJellyfinPlaybackMainDeps: { - getSocketPath: () => appState.mpvSocketPath, - getLaunchMode: () => getResolvedConfig().mpv.launchMode, - platform: process.platform, - execPath: process.execPath, - getPluginRuntimeConfig: () => ({ - socketPath: appState.mpvSocketPath, - binaryPath: getResolvedConfig().mpv.subminerBinaryPath, - backend: getResolvedConfig().mpv.backend, - autoStart: getResolvedConfig().mpv.autoStartSubMiner, - autoStartVisibleOverlay: getResolvedConfig().auto_start_overlay, - autoStartPauseUntilReady: getResolvedConfig().mpv.pauseUntilOverlayReady, - texthookerEnabled: getResolvedConfig().texthooker.launchAtStartup, - aniskipEnabled: getResolvedConfig().mpv.aniskipEnabled, - aniskipButtonKey: getResolvedConfig().mpv.aniskipButtonKey, - }), - defaultMpvLogPath: DEFAULT_MPV_LOG_PATH, - defaultMpvArgs: MPV_JELLYFIN_DEFAULT_ARGS, - removeSocketPath: (socketPath) => { - fs.rmSync(socketPath, { force: true }); - }, - spawnMpv: (args) => (0, node_child_process_1.spawn)('mpv', args, { - detached: true, - stdio: 'ignore', - }), - logWarn: (message, error) => logger.warn(message, error), - logInfo: (message) => logger.info(message), - }, - ensureMpvConnectedForJellyfinPlaybackMainDeps: { - getMpvClient: () => appState.mpvClient, - setMpvClient: (client) => { - appState.mpvClient = client; - }, - createMpvClient: () => createMpvClientRuntimeService(), - getAutoLaunchInFlight: () => jellyfinMpvAutoLaunchInFlight, - setAutoLaunchInFlight: (promise) => { - jellyfinMpvAutoLaunchInFlight = promise; - }, - connectTimeoutMs: JELLYFIN_MPV_CONNECT_TIMEOUT_MS, - autoLaunchTimeoutMs: JELLYFIN_MPV_AUTO_LAUNCH_TIMEOUT_MS, - }, - preloadJellyfinExternalSubtitlesMainDeps: { - listJellyfinSubtitleTracks: (session, clientInfo, itemId) => (0, services_1.listJellyfinSubtitleTracksRuntime)(session, clientInfo, itemId), - getMpvClient: () => appState.mpvClient, - sendMpvCommand: (command) => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command); - }, - wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - logDebug: (message, error) => { - logger.debug(message, error); - }, - }, - playJellyfinItemInMpvMainDeps: { - getMpvClient: () => appState.mpvClient, - resolvePlaybackPlan: (params) => (0, services_1.resolveJellyfinPlaybackPlanRuntime)(params.session, params.clientInfo, params.jellyfinConfig, { - itemId: params.itemId, - audioStreamIndex: params.audioStreamIndex ?? undefined, - subtitleStreamIndex: params.subtitleStreamIndex ?? undefined, - }), - applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient), - sendMpvCommand: (command) => (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command), - armQuitOnDisconnect: () => { - jellyfinPlayQuitOnDisconnectArmed = false; - setTimeout(() => { - jellyfinPlayQuitOnDisconnectArmed = true; - }, 3000); - }, - schedule: (callback, delayMs) => { - setTimeout(callback, delayMs); - }, - convertTicksToSeconds: (ticks) => (0, services_1.jellyfinTicksToSecondsRuntime)(ticks), - setActivePlayback: (state) => { - activeJellyfinRemotePlayback = state; - }, - setLastProgressAtMs: (value) => { - jellyfinRemoteLastProgressAtMs = value; - }, - reportPlaying: (payload) => { - void appState.jellyfinRemoteSession?.reportPlaying(payload); - }, - showMpvOsd: (text) => { - showMpvOsd(text); - }, - }, - remoteComposerOptions: { - getConfiguredSession: () => (0, jellyfin_1.getConfiguredJellyfinSession)(getResolvedJellyfinConfig()), - logWarn: (message) => logger.warn(message), - getMpvClient: () => appState.mpvClient, - sendMpvCommand: (client, command) => (0, services_1.sendMpvCommandRuntime)(client, command), - jellyfinTicksToSeconds: (ticks) => (0, services_1.jellyfinTicksToSecondsRuntime)(ticks), - getActivePlayback: () => activeJellyfinRemotePlayback, - clearActivePlayback: () => { - activeJellyfinRemotePlayback = null; - }, - getSession: () => appState.jellyfinRemoteSession, - getNow: () => Date.now(), - getLastProgressAtMs: () => jellyfinRemoteLastProgressAtMs, - setLastProgressAtMs: (value) => { - jellyfinRemoteLastProgressAtMs = value; - }, - progressIntervalMs: JELLYFIN_REMOTE_PROGRESS_INTERVAL_MS, - ticksPerSecond: JELLYFIN_TICKS_PER_SECOND, - logDebug: (message, error) => logger.debug(message, error), - }, - handleJellyfinAuthCommandsMainDeps: { - patchRawConfig: (patch) => { - configService.patchRawConfig(patch); - refreshTrayMenuIfPresent(); - }, - authenticateWithPassword: (serverUrl, username, password, clientInfo) => (0, services_1.authenticateWithPasswordRuntime)(serverUrl, username, password, clientInfo), - saveStoredSession: (session) => jellyfinTokenStore.saveSession(session), - clearStoredSession: () => (0, jellyfin_tray_discovery_1.clearJellyfinAuthSessionAndRefreshTray)(getJellyfinTrayDiscoveryDeps()), - logInfo: (message) => logger.info(message), - }, - handleJellyfinListCommandsMainDeps: { - listJellyfinLibraries: (session, clientInfo) => (0, services_1.listJellyfinLibrariesRuntime)(session, clientInfo), - listJellyfinItems: (session, clientInfo, params) => (0, services_1.listJellyfinItemsRuntime)(session, clientInfo, params), - listJellyfinSubtitleTracks: (session, clientInfo, itemId) => (0, services_1.listJellyfinSubtitleTracksRuntime)(session, clientInfo, itemId), - writeJellyfinPreviewAuth: (responsePath, payload) => { - fs.mkdirSync(path.dirname(responsePath), { recursive: true }); - fs.writeFileSync(responsePath, JSON.stringify(payload, null, 2), 'utf-8'); - }, - logInfo: (message) => logger.info(message), - }, - handleJellyfinPlayCommandMainDeps: { - logWarn: (message) => logger.warn(message), - }, - handleJellyfinRemoteAnnounceCommandMainDeps: { - getRemoteSession: () => appState.jellyfinRemoteSession, - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), - }, - startJellyfinRemoteSessionMainDeps: { - getCurrentSession: () => appState.jellyfinRemoteSession, - setCurrentSession: (session) => { - appState.jellyfinRemoteSession = session; - }, - createRemoteSessionService: (options) => new services_1.JellyfinRemoteSessionService(options), - defaultDeviceId: config_2.DEFAULT_CONFIG.jellyfin.deviceId, - defaultClientName: config_2.DEFAULT_CONFIG.jellyfin.clientName, - defaultClientVersion: config_2.DEFAULT_CONFIG.jellyfin.clientVersion, - logInfo: (message) => logger.info(message), - logWarn: (message, details) => logger.warn(message, details), - }, - stopJellyfinRemoteSessionMainDeps: { - getCurrentSession: () => appState.jellyfinRemoteSession, - setCurrentSession: (session) => { - appState.jellyfinRemoteSession = session; - }, - clearActivePlayback: () => { - activeJellyfinRemotePlayback = null; - }, - }, - runJellyfinCommandMainDeps: { - defaultServerUrl: config_2.DEFAULT_CONFIG.jellyfin.serverUrl, - }, - maybeFocusExistingJellyfinSetupWindowMainDeps: { - getSetupWindow: () => appState.jellyfinSetupWindow, - }, - openJellyfinSetupWindowMainDeps: { - createSetupWindow: (0, setup_window_factory_1.createCreateJellyfinSetupWindowHandler)({ - createBrowserWindow: (options) => new electron_1.BrowserWindow(options), - }), - buildSetupFormHtml: (state) => (0, jellyfin_1.buildJellyfinSetupFormHtml)(state), - parseSubmissionUrl: (rawUrl) => (0, jellyfin_1.parseJellyfinSetupSubmissionUrl)(rawUrl), - authenticateWithPassword: (server, username, password, clientInfo) => (0, services_1.authenticateWithPasswordRuntime)(server, username, password, clientInfo), - saveStoredSession: (session) => jellyfinTokenStore.saveSession(session), - clearStoredSession: () => (0, jellyfin_tray_discovery_1.clearJellyfinAuthSessionAndRefreshTray)(getJellyfinTrayDiscoveryDeps()), - patchJellyfinConfig: (session) => { - const clientInfo = getJellyfinClientInfo(); - const recentServers = (0, jellyfin_1.mergeJellyfinRecentServers)(session.serverUrl, getResolvedConfig().jellyfin.recentServers || []); - configService.patchRawConfig({ - jellyfin: { - enabled: true, - serverUrl: session.serverUrl, - username: session.username, - deviceId: clientInfo.deviceId, - clientName: clientInfo.clientName, - clientVersion: clientInfo.clientVersion, - recentServers, - }, - }); - refreshTrayMenuIfPresent(); - }, - persistAuthenticatedSession: (session, clientInfo) => (0, jellyfin_1.persistJellyfinAuthSession)({ - session, - clientInfo, - existingRecentServers: getResolvedConfig().jellyfin.recentServers || [], - saveStoredSession: (storedSession) => jellyfinTokenStore.saveSession(storedSession), - patchRawConfig: (patch) => { - configService.patchRawConfig(patch); - refreshTrayMenuIfPresent(); - }, - }), - logInfo: (message) => logger.info(message), - logError: (message, error) => logger.error(message, error), - showMpvOsd: (message) => showMpvOsd(message), - clearSetupWindow: () => { - appState.jellyfinSetupWindow = null; - }, - setSetupWindow: (window) => { - appState.jellyfinSetupWindow = window; - }, - encodeURIComponent: (value) => encodeURIComponent(value), - defaultServerUrl: config_2.DEFAULT_CONFIG.jellyfin.serverUrl || 'http://127.0.0.1:8096', - hasStoredSession: () => Boolean(jellyfinTokenStore.loadSession()), - }, -}); -const maybeFocusExistingFirstRunSetupWindow = (0, first_run_setup_window_1.createMaybeFocusExistingFirstRunSetupWindowHandler)({ - getSetupWindow: () => appState.firstRunSetupWindow, -}); -const openFirstRunSetupWindowHandler = (0, first_run_setup_window_1.createOpenFirstRunSetupWindowHandler)({ - maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow, - createSetupWindow: (0, setup_window_factory_1.createCreateFirstRunSetupWindowHandler)({ - createBrowserWindow: (options) => new electron_1.BrowserWindow(options), - }), - getSetupSnapshot: async () => { - const snapshot = await firstRunSetupService.getSetupStatus(); - const mpvExecutablePath = getResolvedConfig().mpv.executablePath; - return { - configReady: snapshot.configReady, - dictionaryCount: snapshot.dictionaryCount, - canFinish: snapshot.canFinish, - externalYomitanConfigured: snapshot.externalYomitanConfigured, - pluginStatus: snapshot.pluginStatus, - pluginInstallPathSummary: snapshot.pluginInstallPathSummary, - legacyMpvPluginPaths: snapshot.legacyMpvPluginPaths, - mpvExecutablePath, - mpvExecutablePathStatus: (0, windows_mpv_launch_1.getConfiguredWindowsMpvPathStatus)(mpvExecutablePath), - windowsMpvShortcuts: snapshot.windowsMpvShortcuts, - commandLineLauncher: snapshot.commandLineLauncher, - message: firstRunSetupMessage, - }; - }, - buildSetupHtml: (model) => (0, first_run_setup_window_1.buildFirstRunSetupHtml)(model), - parseSubmissionUrl: (rawUrl) => (0, first_run_setup_window_1.parseFirstRunSetupSubmissionUrl)(rawUrl), - handleAction: async (submission) => { - if (submission.action === 'remove-legacy-plugin') { - const snapshot = await firstRunSetupService.removeLegacyMpvPlugin(); - firstRunSetupMessage = snapshot.message; - return; - } - if (submission.action === 'configure-mpv-executable-path') { - const mpvExecutablePath = submission.mpvExecutablePath?.trim() ?? ''; - const pathStatus = (0, windows_mpv_launch_1.getConfiguredWindowsMpvPathStatus)(mpvExecutablePath); - configService.patchRawConfig({ - mpv: { - executablePath: mpvExecutablePath, - }, - }); - firstRunSetupMessage = - pathStatus === 'invalid' - ? `Saved mpv executable path, but the file was not found: ${mpvExecutablePath}` - : mpvExecutablePath - ? `Saved mpv executable path: ${mpvExecutablePath}` - : 'Cleared mpv executable path. SubMiner will auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.'; - return; - } - if (submission.action === 'configure-windows-mpv-shortcuts') { - const snapshot = await firstRunSetupService.configureWindowsMpvShortcuts({ - startMenuEnabled: submission.startMenuEnabled === true, - desktopEnabled: submission.desktopEnabled === true, - }); - firstRunSetupMessage = snapshot.message; - return; - } - if (submission.action === 'install-bun') { - const snapshot = await firstRunSetupService.installBun(); - firstRunSetupMessage = snapshot.message; - return; - } - if (submission.action === 'install-command-line-launcher') { - const snapshot = await firstRunSetupService.installCommandLineLauncher(); - firstRunSetupMessage = snapshot.message; - return; - } - if (submission.action === 'open-yomitan-settings') { - firstRunSetupMessage = openYomitanSettings() - ? 'Opened Yomitan settings. Install dictionaries, then refresh status.' - : 'Yomitan settings are unavailable while external read-only profile mode is enabled.'; - return; - } - if (submission.action === 'refresh') { - const snapshot = await firstRunSetupService.refreshStatus('Status refreshed.'); - firstRunSetupMessage = snapshot.message; - return; - } - const snapshot = await firstRunSetupService.markSetupCompleted(); - if (snapshot.state.status === 'completed') { - firstRunSetupMessage = null; - return { closeWindow: true }; - } - firstRunSetupMessage = - (0, first_run_setup_service_1.getFirstRunSetupCompletionMessage)(snapshot) ?? - 'Finish setup requires the mpv plugin and Yomitan dictionaries.'; - return; - }, - markSetupInProgress: async () => { - firstRunSetupMessage = null; - await firstRunSetupService.markSetupInProgress(); - }, - markSetupCancelled: async () => { - firstRunSetupMessage = null; - await firstRunSetupService.markSetupCancelled(); - }, - isSetupCompleted: () => firstRunSetupService.isSetupCompleted(), - shouldQuitWhenClosedIncomplete: () => !appState.backgroundMode, - shouldQuitWhenClosedCompleted: () => Boolean(appState.initialArgs && (0, first_run_setup_service_1.isStandaloneFirstRunSetupCommand)(appState.initialArgs)), - quitApp: () => requestAppQuit(), - clearSetupWindow: () => { - appState.firstRunSetupWindow = null; - }, - setSetupWindow: (window) => { - appState.firstRunSetupWindow = window; - }, - encodeURIComponent: (value) => encodeURIComponent(value), - logError: (message, error) => logger.error(message, error), -}); -function openFirstRunSetupWindow(force = false) { - if (!force && firstRunSetupService.isSetupCompleted()) { - return; - } - openFirstRunSetupWindowHandler(); -} -const { notifyAnilistSetup, consumeAnilistSetupTokenFromUrl, handleAnilistSetupProtocolUrl, registerSubminerProtocolClient, } = (0, composers_1.composeAnilistSetupHandlers)({ - notifyDeps: { - hasMpvClient: () => Boolean(appState.mpvClient), - showMpvOsd: (message) => showMpvOsd(message), - showDesktopNotification: (title, options) => (0, utils_2.showDesktopNotification)(title, options), - logInfo: (message) => logger.info(message), - }, - consumeTokenDeps: { - consumeAnilistSetupCallbackUrl: anilist_1.consumeAnilistSetupCallbackUrl, - saveToken: (token) => anilistTokenStore.saveToken(token), - setCachedToken: (token) => { - anilistCachedAccessToken = token; - }, - setResolvedState: (resolvedAt) => { - anilistStateRuntime.setClientSecretState({ - status: 'resolved', - source: 'stored', - message: 'saved token from AniList login', - resolvedAt, - errorAt: null, - }); - }, - setSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - onSuccess: () => { - notifyAnilistSetup('AniList login success'); - }, - closeWindow: () => { - if (appState.anilistSetupWindow && !appState.anilistSetupWindow.isDestroyed()) { - appState.anilistSetupWindow.close(); - } - }, - }, - handleProtocolDeps: { - consumeAnilistSetupTokenFromUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), - logWarn: (message, details) => logger.warn(message, details), - }, - registerProtocolClientDeps: { - isDefaultApp: () => Boolean(process.defaultApp), - getArgv: () => process.argv, - execPath: process.execPath, - resolvePath: (value) => path.resolve(value), - setAsDefaultProtocolClient: (scheme, appPath, args) => appPath - ? electron_1.app.setAsDefaultProtocolClient(scheme, appPath, args) - : electron_1.app.setAsDefaultProtocolClient(scheme), - logDebug: (message, details) => logger.debug(message, details), - }, -}); -const maybeFocusExistingAnilistSetupWindow = (0, anilist_1.createMaybeFocusExistingAnilistSetupWindowHandler)({ - getSetupWindow: () => appState.anilistSetupWindow, -}); -const buildOpenAnilistSetupWindowMainDepsHandler = (0, anilist_1.createBuildOpenAnilistSetupWindowMainDepsHandler)({ - maybeFocusExistingSetupWindow: maybeFocusExistingAnilistSetupWindow, - createSetupWindow: (0, setup_window_factory_1.createCreateAnilistSetupWindowHandler)({ - createBrowserWindow: (options) => new electron_1.BrowserWindow(options), - }), - buildAuthorizeUrl: () => (0, anilist_1.buildAnilistSetupUrl)({ - authorizeUrl: ANILIST_SETUP_CLIENT_ID_URL, - clientId: ANILIST_DEFAULT_CLIENT_ID, - responseType: ANILIST_SETUP_RESPONSE_TYPE, - }), - consumeCallbackUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), - openSetupInBrowser: (authorizeUrl) => (0, anilist_1.openAnilistSetupInBrowser)({ - authorizeUrl, - openExternal: (url) => electron_1.shell.openExternal(url), - logError: (message, error) => logger.error(message, error), - }), - loadManualTokenEntry: (setupWindow, authorizeUrl) => (0, anilist_1.loadAnilistManualTokenEntry)({ - setupWindow: setupWindow, - authorizeUrl, - developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, - logWarn: (message, data) => logger.warn(message, data), - }), - redirectUri: ANILIST_REDIRECT_URI, - developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, - isAllowedExternalUrl: (url) => (0, anilist_url_guard_1.isAllowedAnilistExternalUrl)(url), - isAllowedNavigationUrl: (url) => (0, anilist_url_guard_1.isAllowedAnilistSetupNavigationUrl)(url), - logWarn: (message, details) => logger.warn(message, details), - logError: (message, details) => logger.error(message, details), - clearSetupWindow: () => { - appState.anilistSetupWindow = null; - }, - setSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - setSetupWindow: (setupWindow) => { - appState.anilistSetupWindow = setupWindow; - }, - openExternal: (url) => { - void electron_1.shell.openExternal(url); - }, -}); -function openAnilistSetupWindow() { - (0, anilist_1.createOpenAnilistSetupWindowHandler)(buildOpenAnilistSetupWindowMainDepsHandler())(); -} -const { refreshAnilistClientSecretState, getCurrentAnilistMediaKey, resetAnilistMediaTracking, getAnilistMediaGuessRuntimeState, setAnilistMediaGuessRuntimeState, recordAnilistMediaDuration, resetAnilistMediaGuessState, maybeProbeAnilistDuration, ensureAnilistMediaGuess, processNextAnilistRetryUpdate, maybeRunAnilistPostWatchUpdate, } = (0, composers_1.composeAnilistTrackingHandlers)({ - refreshClientSecretMainDeps: { - getResolvedConfig: () => getResolvedConfig(), - isAnilistTrackingEnabled: (config) => (0, anilist_1.isAnilistTrackingEnabled)(config), - getCachedAccessToken: () => anilistCachedAccessToken, - setCachedAccessToken: (token) => { - anilistCachedAccessToken = token; - }, - saveStoredToken: (token) => { - anilistTokenStore.saveToken(token); - }, - loadStoredToken: () => anilistTokenStore.loadToken(), - setClientSecretState: (state) => { - anilistStateRuntime.setClientSecretState(state); - }, - getAnilistSetupPageOpened: () => appState.anilistSetupPageOpened, - setAnilistSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - openAnilistSetupWindow: () => { - openAnilistSetupWindow(); - }, - now: () => Date.now(), - }, - getCurrentMediaKeyMainDeps: { - getCurrentMediaPath: () => appState.currentMediaPath, - }, - resetMediaTrackingMainDeps: { - setMediaKey: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaKey: value }); - }, - setMediaDurationSec: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaDurationSec: value }); - }, - setMediaGuess: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuess: value }); - }, - setMediaGuessPromise: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuessPromise: value }); - }, - setLastDurationProbeAtMs: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { lastDurationProbeAtMs: value }); - }, - }, - getMediaGuessRuntimeStateMainDeps: { - getMediaKey: () => anilistMediaGuessRuntimeState.mediaKey, - getMediaDurationSec: () => anilistMediaGuessRuntimeState.mediaDurationSec, - getMediaGuess: () => anilistMediaGuessRuntimeState.mediaGuess, - getMediaGuessPromise: () => anilistMediaGuessRuntimeState.mediaGuessPromise, - getLastDurationProbeAtMs: () => anilistMediaGuessRuntimeState.lastDurationProbeAtMs, - }, - setMediaGuessRuntimeStateMainDeps: { - setMediaKey: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaKey: value }); - }, - setMediaDurationSec: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaDurationSec: value }); - }, - setMediaGuess: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuess: value }); - }, - setMediaGuessPromise: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuessPromise: value }); - }, - setLastDurationProbeAtMs: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { lastDurationProbeAtMs: value }); - }, - }, - recordMediaDurationMainDeps: { - getCurrentMediaKey: () => getCurrentAnilistMediaKey(), - getState: () => getAnilistMediaGuessRuntimeState(), - setState: (state) => { - setAnilistMediaGuessRuntimeState(state); - }, - }, - resetMediaGuessStateMainDeps: { - setMediaGuess: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuess: value }); - }, - setMediaGuessPromise: (value) => { - anilistMediaGuessRuntimeState = (0, state_1.transitionAnilistMediaGuessRuntimeState)(anilistMediaGuessRuntimeState, { mediaGuessPromise: value }); - }, - }, - maybeProbeDurationMainDeps: { - getState: () => getAnilistMediaGuessRuntimeState(), - setState: (state) => { - setAnilistMediaGuessRuntimeState(state); - }, - durationRetryIntervalMs: ANILIST_DURATION_RETRY_INTERVAL_MS, - now: () => Date.now(), - requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'), - logWarn: (message, error) => logger.warn(message, error), - }, - ensureMediaGuessMainDeps: { - getState: () => getAnilistMediaGuessRuntimeState(), - setState: (state) => { - setAnilistMediaGuessRuntimeState(state); - }, - resolveMediaPathForJimaku: (currentMediaPath) => mediaRuntime.resolveMediaPathForJimaku(currentMediaPath), - getCurrentMediaPath: () => appState.currentMediaPath, - getCurrentMediaTitle: () => appState.currentMediaTitle, - guessAnilistMediaInfo: (mediaPath, mediaTitle) => (0, anilist_updater_1.guessAnilistMediaInfo)(mediaPath, mediaTitle), - }, - processNextRetryUpdateMainDeps: { - nextReady: () => anilistUpdateQueue.nextReady(), - refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(), - setLastAttemptAt: (value) => { - appState.anilistRetryQueueState = (0, state_1.transitionAnilistRetryQueueLastAttemptAt)(appState.anilistRetryQueueState, value); - }, - setLastError: (value) => { - appState.anilistRetryQueueState = (0, state_1.transitionAnilistRetryQueueLastError)(appState.anilistRetryQueueState, value); - }, - refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(), - updateAnilistPostWatchProgress: (accessToken, title, episode, season) => (0, anilist_updater_1.updateAnilistPostWatchProgress)(accessToken, title, episode, { - rateLimiter: anilistRateLimiter, - season, - }), - markSuccess: (key) => { - anilistUpdateQueue.markSuccess(key); - }, - rememberAttemptedUpdateKey: (key) => { - rememberAnilistAttemptedUpdate(key); - }, - markFailure: (key, message) => { - anilistUpdateQueue.markFailure(key, message); - }, - logInfo: (message) => logger.info(message), - now: () => Date.now(), - }, - maybeRunPostWatchUpdateMainDeps: { - getInFlight: () => anilistUpdateInFlightState.inFlight, - setInFlight: (value) => { - anilistUpdateInFlightState = (0, state_1.transitionAnilistUpdateInFlightState)(anilistUpdateInFlightState, value); - }, - getResolvedConfig: () => getResolvedConfig(), - isAnilistTrackingEnabled: (config) => (0, anilist_1.isAnilistTrackingEnabled)(config), - getCurrentMediaKey: () => getCurrentAnilistMediaKey(), - hasMpvClient: () => Boolean(appState.mpvClient), - getTrackedMediaKey: () => anilistMediaGuessRuntimeState.mediaKey, - resetTrackedMedia: (mediaKey) => { - resetAnilistMediaTracking(mediaKey); - }, - getWatchedSeconds: () => appState.mpvClient?.currentTimePos ?? Number.NaN, - maybeProbeAnilistDuration: (mediaKey, options) => maybeProbeAnilistDuration(mediaKey, options), - ensureAnilistMediaGuess: (mediaKey) => ensureAnilistMediaGuess(mediaKey), - hasAttemptedUpdateKey: (key) => anilistAttemptedUpdateKeys.has(key), - processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(), - refreshAnilistClientSecretState: () => refreshAnilistClientSecretState(), - enqueueRetry: (key, title, episode, season) => { - anilistUpdateQueue.enqueue(key, title, episode, season); - }, - markRetryFailure: (key, message) => { - anilistUpdateQueue.markFailure(key, message); - }, - markRetrySuccess: (key) => { - anilistUpdateQueue.markSuccess(key); - }, - refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(), - updateAnilistPostWatchProgress: (accessToken, title, episode, season) => (0, anilist_updater_1.updateAnilistPostWatchProgress)(accessToken, title, episode, { - rateLimiter: anilistRateLimiter, - season, - }), - rememberAttemptedUpdateKey: (key) => { - rememberAnilistAttemptedUpdate(key); - }, - showMpvOsd: (message) => showMpvOsd(message), - logInfo: (message) => logger.info(message), - logWarn: (message) => logger.warn(message), - minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS, - minWatchRatio: watch_threshold_1.DEFAULT_MIN_WATCH_RATIO, - }, -}); -function refreshAnilistClientSecretStateIfEnabled(options) { - if (!(0, anilist_1.isAnilistTrackingEnabled)(getResolvedConfig())) { - return Promise.resolve(null); - } - return refreshAnilistClientSecretState(options); -} -const rememberAnilistAttemptedUpdate = (key) => { - (0, anilist_1.rememberAnilistAttemptedUpdateKey)(anilistAttemptedUpdateKeys, key, ANILIST_MAX_ATTEMPTED_UPDATE_KEYS); -}; -const buildLoadSubtitlePositionMainDepsHandler = (0, overlay_1.createBuildLoadSubtitlePositionMainDepsHandler)({ - loadSubtitlePositionCore: () => (0, services_1.loadSubtitlePosition)({ - currentMediaPath: appState.currentMediaPath, - fallbackPosition: getResolvedConfig().subtitlePosition, - subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, - }), - setSubtitlePosition: (position) => { - appState.subtitlePosition = position; - }, -}); -const loadSubtitlePositionMainDeps = buildLoadSubtitlePositionMainDepsHandler(); -const loadSubtitlePosition = (0, overlay_1.createLoadSubtitlePositionHandler)(loadSubtitlePositionMainDeps); -const buildSaveSubtitlePositionMainDepsHandler = (0, overlay_1.createBuildSaveSubtitlePositionMainDepsHandler)({ - saveSubtitlePositionCore: (position) => { - (0, services_1.saveSubtitlePosition)({ - position, - currentMediaPath: appState.currentMediaPath, - subtitlePositionsDir: SUBTITLE_POSITIONS_DIR, - onQueuePending: (queued) => { - appState.pendingSubtitlePosition = queued; - }, - onPersisted: () => { - appState.pendingSubtitlePosition = null; - }, - }); - }, - setSubtitlePosition: (position) => { - appState.subtitlePosition = position; - }, -}); -const saveSubtitlePositionMainDeps = buildSaveSubtitlePositionMainDepsHandler(); -const saveSubtitlePosition = (0, overlay_1.createSaveSubtitlePositionHandler)(saveSubtitlePositionMainDeps); -registerSubminerProtocolClient(); -let flushPendingMpvLogWrites = () => { }; -const { registerProtocolUrlHandlers: registerProtocolUrlHandlersHandler, onWillQuitCleanup: onWillQuitCleanupHandler, shouldRestoreWindowsOnActivate: shouldRestoreWindowsOnActivateHandler, restoreWindowsOnActivate: restoreWindowsOnActivateHandler, } = (0, composers_1.composeStartupLifecycleHandlers)({ - registerProtocolUrlHandlersMainDeps: { - registerOpenUrl: (listener) => { - electron_1.app.on('open-url', listener); - }, - registerSecondInstance: (listener) => { - (0, early_single_instance_1.registerSecondInstanceHandlerEarly)(electron_1.app, listener); - }, - handleAnilistSetupProtocolUrl: (rawUrl) => handleAnilistSetupProtocolUrl(rawUrl), - findAnilistSetupDeepLinkArgvUrl: (argv) => (0, anilist_1.findAnilistSetupDeepLinkArgvUrl)(argv), - logUnhandledOpenUrl: (rawUrl) => { - logger.warn('Unhandled app protocol URL', { rawUrl }); - }, - logUnhandledSecondInstanceUrl: (rawUrl) => { - logger.warn('Unhandled second-instance protocol URL', { rawUrl }); - }, - }, - onWillQuitCleanupMainDeps: { - destroyTray: () => destroyTray(), - stopConfigHotReload: () => configHotReloadRuntime.stop(), - restorePreviousSecondarySubVisibility: () => restorePreviousSecondarySubVisibility(), - restoreMpvSubVisibility: () => { - restoreOverlayMpvSubtitles(); - }, - unregisterAllGlobalShortcuts: () => electron_1.globalShortcut.unregisterAll(), - stopSubtitleWebsocket: () => { - subtitleWsService.stop(); - annotationSubtitleWsService.stop(); - }, - stopTexthookerService: () => texthookerService.stop(), - clearWindowsVisibleOverlayForegroundPollLoop: () => clearWindowsVisibleOverlayForegroundPollLoop(), - clearLinuxMpvFullscreenOverlayRefreshTimeouts: () => { - cancelLinuxMpvFullscreenOverlayRefreshBurst = null; - (0, linux_mpv_fullscreen_overlay_refresh_1.clearLinuxMpvFullscreenOverlayRefreshTimeouts)(); - }, - getMainOverlayWindow: () => overlayManager.getMainWindow(), - clearMainOverlayWindow: () => overlayManager.setMainWindow(null), - getModalOverlayWindow: () => overlayManager.getModalWindow(), - clearModalOverlayWindow: () => overlayManager.setModalWindow(null), - getYomitanParserWindow: () => appState.yomitanParserWindow, - clearYomitanParserState: () => { - appState.yomitanParserWindow = null; - appState.yomitanParserReadyPromise = null; - appState.yomitanParserInitPromise = null; - appState.yomitanSession = null; - }, - getWindowTracker: () => appState.windowTracker, - flushMpvLog: () => flushPendingMpvLogWrites(), - getMpvSocket: () => appState.mpvClient?.socket ?? null, - getReconnectTimer: () => appState.reconnectTimer, - clearReconnectTimerRef: () => { - appState.reconnectTimer = null; - }, - getSubtitleTimingTracker: () => appState.subtitleTimingTracker, - getImmersionTracker: () => appState.immersionTracker, - clearImmersionTracker: () => { - stopStatsServer(); - appState.statsServer = null; - appState.immersionTracker = null; - }, - getAnkiIntegration: () => appState.ankiIntegration, - getAnilistSetupWindow: () => appState.anilistSetupWindow, - clearAnilistSetupWindow: () => { - appState.anilistSetupWindow = null; - }, - getJellyfinSetupWindow: () => appState.jellyfinSetupWindow, - clearJellyfinSetupWindow: () => { - appState.jellyfinSetupWindow = null; - }, - getFirstRunSetupWindow: () => appState.firstRunSetupWindow, - clearFirstRunSetupWindow: () => { - appState.firstRunSetupWindow = null; - }, - getYomitanSettingsWindow: () => appState.yomitanSettingsWindow, - clearYomitanSettingsWindow: () => { - appState.yomitanSettingsWindow = null; - }, - stopJellyfinRemoteSession: () => stopJellyfinRemoteSession(), - stopDiscordPresenceService: () => { - void appState.discordPresenceService?.stop(); - appState.discordPresenceService = null; - }, - }, - shouldRestoreWindowsOnActivateMainDeps: { - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - getAllWindowCount: () => electron_1.BrowserWindow.getAllWindows().length, - }, - restoreWindowsOnActivateMainDeps: { - createMainWindow: () => { - createMainWindow(); - }, - updateVisibleOverlayVisibility: () => { - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - }, - syncOverlayMpvSubtitleSuppression: () => { - syncOverlayMpvSubtitleSuppression(); - }, - }, -}); -registerProtocolUrlHandlersHandler(); -const statsDistPath = path.join(__dirname, '..', 'stats', 'dist'); -const statsPreloadPath = path.join(__dirname, 'preload-stats.js'); -const startLocalStatsServer = () => { - const tracker = appState.immersionTracker; - if (!tracker) { - throw new Error('Immersion tracker failed to initialize.'); - } - if (!statsServer) { - const yomitanDeps = { - getYomitanExt: () => appState.yomitanExt, - getYomitanSession: () => appState.yomitanSession, - getYomitanParserWindow: () => appState.yomitanParserWindow, - setYomitanParserWindow: (w) => { - appState.yomitanParserWindow = w; - }, - getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise, - setYomitanParserReadyPromise: (p) => { - appState.yomitanParserReadyPromise = p; - }, - getYomitanParserInitPromise: () => appState.yomitanParserInitPromise, - setYomitanParserInitPromise: (p) => { - appState.yomitanParserInitPromise = p; - }, - }; - const yomitanLogger = (0, logger_1.createLogger)('main:yomitan-stats'); - statsServer = (0, stats_server_1.startStatsServer)({ - port: getResolvedConfig().stats.serverPort, - staticDir: statsDistPath, - tracker, - knownWordCachePath: path.join(USER_DATA_PATH, 'known-words-cache.json'), - mpvSocketPath: appState.mpvSocketPath, - ankiConnectConfig: getResolvedConfig().ankiConnect, - anilistRateLimiter, - resolveAnkiNoteId: (noteId) => appState.ankiIntegration?.resolveCurrentNoteId(noteId) ?? noteId, - addYomitanNote: async (word) => { - const ankiUrl = getResolvedConfig().ankiConnect.url || 'http://127.0.0.1:8765'; - await (0, services_1.syncYomitanDefaultAnkiServer)(ankiUrl, yomitanDeps, yomitanLogger, { - forceOverride: true, - }); - const result = await (0, services_1.addYomitanNoteViaSearch)(word, yomitanDeps, yomitanLogger); - if (result.noteId && result.duplicateNoteIds.length > 0) { - appState.ankiIntegration?.trackDuplicateNoteIdsForNote(result.noteId, result.duplicateNoteIds); - } - return result.noteId; - }, - }); - appState.statsServer = statsServer; - } - appState.statsServer = statsServer; -}; -const ensureStatsServerStarted = (0, stats_server_routing_1.createEnsureStatsServerUrlHandler)({ - currentPid: process.pid, - readBackgroundState: () => (0, stats_daemon_1.readBackgroundStatsServerState)(statsDaemonStatePath), - removeBackgroundState: () => { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - }, - isProcessAlive: (pid) => (0, stats_daemon_1.isBackgroundStatsServerProcessAlive)(pid), - hasLocalStatsServer: () => statsServer !== null, - startLocalStatsServer, - getConfiguredPort: () => getResolvedConfig().stats.serverPort, -}); -const ensureBackgroundStatsServerStarted = () => { - const liveDaemon = readLiveBackgroundStatsDaemonState(); - if (liveDaemon && liveDaemon.pid !== process.pid) { - return { - url: (0, stats_daemon_1.resolveBackgroundStatsServerUrl)(liveDaemon), - runningInCurrentProcess: false, - }; - } - appState.statsStartupInProgress = true; - try { - ensureImmersionTrackerStarted(); - } - finally { - appState.statsStartupInProgress = false; - } - const port = getResolvedConfig().stats.serverPort; - const result = ensureStatsServerStarted(); - if (result.source === 'local') { - (0, stats_daemon_1.writeBackgroundStatsServerState)(statsDaemonStatePath, { - pid: process.pid, - port, - startedAtMs: Date.now(), - }); - } - return { url: result.url, runningInCurrentProcess: result.source === 'local' }; -}; -const stopBackgroundStatsServer = async () => { - const state = (0, stats_daemon_1.readBackgroundStatsServerState)(statsDaemonStatePath); - if (!state) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return { ok: true, stale: true }; - } - if (!(0, stats_daemon_1.isBackgroundStatsServerProcessAlive)(state.pid)) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return { ok: true, stale: true }; - } - try { - process.kill(state.pid, 'SIGTERM'); - } - catch (error) { - if (error?.code === 'ESRCH') { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return { ok: true, stale: true }; - } - if (error?.code === 'EPERM') { - throw new Error(`Insufficient permissions to stop background stats server (pid ${state.pid}).`); - } - throw error; - } - const deadline = Date.now() + 2_000; - while (Date.now() < deadline) { - if (!(0, stats_daemon_1.isBackgroundStatsServerProcessAlive)(state.pid)) { - (0, stats_daemon_1.removeBackgroundStatsServerState)(statsDaemonStatePath); - return { ok: true, stale: false }; - } - await new Promise((resolve) => setTimeout(resolve, 50)); - } - throw new Error('Timed out stopping background stats server.'); -}; -const resolveLegacyVocabularyPos = async (row) => { - const tokenizer = appState.mecabTokenizer; - if (!tokenizer) { - return null; - } - const lookupTexts = [...new Set([row.headword, row.word, row.reading ?? ''])] - .map((value) => value.trim()) - .filter((value) => value.length > 0); - for (const lookupText of lookupTexts) { - const tokens = await tokenizer.tokenize(lookupText); - const resolved = (0, legacy_vocabulary_pos_1.resolveLegacyVocabularyPosFromTokens)(lookupText, tokens); - if (resolved) { - return resolved; - } - } - return null; -}; -const immersionTrackerStartupMainDeps = { - getResolvedConfig: () => getResolvedConfig(), - getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(), - createTrackerService: (params) => new services_1.ImmersionTrackerService({ - ...params, - resolveLegacyVocabularyPos, - }), - setTracker: (tracker) => { - const trackerHasChanged = appState.immersionTracker !== null && appState.immersionTracker !== tracker; - if (trackerHasChanged && appState.statsServer) { - stopStatsServer(); - appState.statsServer = null; - } - appState.immersionTracker = tracker; - appState.immersionTracker?.setCoverArtFetcher(statsCoverArtFetcher); - if (tracker) { - // Start HTTP stats server - if (!appState.statsServer) { - const config = getResolvedConfig(); - if (config.stats.autoStartServer) { - ensureStatsServerStarted(); - } - } - // Register stats overlay toggle IPC handler (idempotent) - (0, stats_window_js_1.registerStatsOverlayToggle)({ - staticDir: statsDistPath, - preloadPath: statsPreloadPath, - getApiBaseUrl: () => ensureStatsServerStarted().url, - getToggleKey: () => getResolvedConfig().stats.toggleKey, - resolveBounds: () => getCurrentOverlayGeometry(), - onVisibilityChanged: (visible) => { - appState.statsOverlayVisible = visible; - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - }, - }); - } - }, - getMpvClient: () => appState.mpvClient, - shouldAutoConnectMpv: () => !appState.statsStartupInProgress, - seedTrackerFromCurrentMedia: () => { - void immersionMediaRuntime.seedFromCurrentMedia(); - }, - logInfo: (message) => logger.info(message), - logDebug: (message) => logger.debug(message), - logWarn: (message, details) => logger.warn(message, details), -}; -const createImmersionTrackerStartup = (0, immersion_startup_1.createImmersionTrackerStartupHandler)((0, immersion_startup_main_deps_1.createBuildImmersionTrackerStartupMainDepsHandler)(immersionTrackerStartupMainDeps)()); -const recordTrackedCardsMined = (count, noteIds) => { - ensureImmersionTrackerStarted(); - appState.immersionTracker?.recordCardsMined(count, noteIds); -}; -const refreshCurrentSubtitleAfterKnownWordUpdate = () => { - subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText); -}; -let hasAttemptedImmersionTrackerStartup = false; -const ensureImmersionTrackerStarted = () => { - if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) { - return; - } - hasAttemptedImmersionTrackerStartup = true; - createImmersionTrackerStartup(); -}; -const statsStartupRuntime = { - ensureStatsServerStarted: () => ensureStatsServerStarted(), - ensureBackgroundStatsServerStarted: () => ensureBackgroundStatsServerStarted(), - stopBackgroundStatsServer: () => stopBackgroundStatsServer(), - ensureImmersionTrackerStarted: () => { - appState.statsStartupInProgress = true; - try { - ensureImmersionTrackerStarted(); - } - finally { - appState.statsStartupInProgress = false; - } - }, -}; -const runStatsCliCommand = (0, stats_cli_command_1.createRunStatsCliCommandHandler)({ - getResolvedConfig: () => getResolvedConfig(), - ensureImmersionTrackerStarted: () => statsStartupRuntime.ensureImmersionTrackerStarted(), - ensureVocabularyCleanupTokenizerReady: async () => { - await createMecabTokenizerAndCheck(); - }, - getImmersionTracker: () => appState.immersionTracker, - ensureStatsServerStarted: () => statsStartupRuntime.ensureStatsServerStarted().url, - ensureBackgroundStatsServerStarted: () => statsStartupRuntime.ensureBackgroundStatsServerStarted(), - stopBackgroundStatsServer: () => statsStartupRuntime.stopBackgroundStatsServer(), - openExternal: (url) => electron_1.shell.openExternal(url), - writeResponse: (responsePath, payload) => { - (0, stats_cli_command_1.writeStatsCliCommandResponse)(responsePath, payload); - }, - exitAppWithCode: (code) => { - process.exitCode = code; - requestAppQuit(); - }, - logInfo: (message) => logger.info(message), - logWarn: (message, error) => logger.warn(message, error), - logError: (message, error) => logger.error(message, error), -}); -async function runHeadlessInitialCommand() { - if (!appState.initialArgs?.refreshKnownWords) { - handleInitialArgs(); - return; - } - const resolvedConfig = getResolvedConfig(); - if (resolvedConfig.ankiConnect.enabled !== true) { - logger.error('Headless known-word refresh failed: AnkiConnect integration not enabled'); - process.exitCode = 1; - requestAppQuit(); - return; - } - const effectiveAnkiConfig = appState.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(resolvedConfig.ankiConnect) ?? - resolvedConfig.ankiConnect; - const integration = new anki_integration_1.AnkiIntegration(effectiveAnkiConfig, new subtitle_timing_tracker_1.SubtitleTimingTracker(), { send: () => undefined }, undefined, undefined, async () => ({ - keepNoteId: 0, - deleteNoteId: 0, - deleteDuplicate: false, - cancelled: true, - }), path.join(USER_DATA_PATH, 'known-words-cache.json'), (0, config_1.mergeAiConfig)(resolvedConfig.ai, resolvedConfig.ankiConnect?.ai)); - try { - await integration.refreshKnownWordCache(); - } - catch (error) { - logger.error('Headless known-word refresh failed:', error); - process.exitCode = 1; - } - finally { - integration.stop(); - requestAppQuit(); - } -} -const { appReadyRuntimeRunner } = (0, composers_1.composeAppReadyRuntime)({ - reloadConfigMainDeps: { - reloadConfigStrict: () => configService.reloadConfigStrict(), - logInfo: (message) => appLogger.logInfo(message), - logDebug: (message) => appLogger.logDebug(message), - logWarning: (message) => appLogger.logWarning(message), - showDesktopNotification: (title, options) => (0, utils_2.showDesktopNotification)(title, options), - startConfigHotReload: () => configHotReloadRuntime.start(), - shouldRefreshAnilistClientSecretState: () => (0, startup_mode_flags_1.shouldRefreshAnilistOnConfigReload)(appState.initialArgs), - refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretStateIfEnabled(options), - failHandlers: { - logError: (details) => logger.error(details), - showErrorBox: (title, details) => electron_1.dialog.showErrorBox(title, details), - quit: () => requestAppQuit(), - }, - }, - criticalConfigErrorMainDeps: { - getConfigPath: () => configService.getConfigPath(), - failHandlers: { - logError: (message) => logger.error(message), - showErrorBox: (title, message) => electron_1.dialog.showErrorBox(title, message), - quit: () => requestAppQuit(), - }, - }, - appReadyRuntimeMainDeps: { - ensureDefaultConfigBootstrap: () => { - (0, setup_state_1.ensureDefaultConfigBootstrap)({ - configDir: CONFIG_DIR, - configFilePaths: (0, setup_state_1.getDefaultConfigFilePaths)(CONFIG_DIR), - generateTemplate: () => (0, config_2.generateConfigTemplate)(config_2.DEFAULT_CONFIG), - }); - }, - loadSubtitlePosition: () => loadSubtitlePosition(), - resolveKeybindings: () => { - appState.keybindings = (0, utils_2.resolveKeybindings)(getResolvedConfig(), config_2.DEFAULT_KEYBINDINGS); - refreshCurrentSessionBindings(); - }, - createMpvClient: () => { - appState.mpvClient = createMpvClientRuntimeService(); - }, - getResolvedConfig: () => getResolvedConfig(), - getConfigWarnings: () => configService.getWarnings(), - logConfigWarning: (warning) => appLogger.logConfigWarning(warning), - setLogLevel: (level, source) => (0, logger_1.setLogLevel)(level, source), - initRuntimeOptionsManager: () => { - appState.runtimeOptionsManager = new runtime_options_1.RuntimeOptionsManager(() => configService.getConfig().ankiConnect, { - applyAnkiPatch: (patch) => { - if (appState.ankiIntegration) { - appState.ankiIntegration.applyRuntimeConfigPatch(patch); - } - }, - getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle, - onOptionsChanged: () => { - subtitleProcessingController.invalidateTokenizationCache(); - subtitlePrefetchService?.onSeek(lastObservedTimePos); - broadcastRuntimeOptionsChanged(); - refreshOverlayShortcuts(); - }, - }); - }, - setSecondarySubMode: (mode) => { - setSecondarySubMode(mode); - }, - defaultSecondarySubMode: 'hover', - defaultWebsocketPort: config_2.DEFAULT_CONFIG.websocket.port, - defaultAnnotationWebsocketPort: config_2.DEFAULT_CONFIG.annotationWebsocket.port, - defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT, - hasMpvWebsocketPlugin: () => (0, services_1.hasMpvWebsocketPlugin)(), - startSubtitleWebsocket: (port) => { - subtitleWsService.start(port, () => appState.currentSubtitleData ?? - (appState.currentSubText - ? { - text: appState.currentSubText, - tokens: null, - startTime: appState.mpvClient?.currentSubStart ?? null, - endTime: appState.mpvClient?.currentSubEnd ?? null, - } - : null), () => ({ - enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, - mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, - })); - }, - startAnnotationWebsocket: (port) => { - annotationSubtitleWsService.start(port, () => appState.currentSubtitleData ?? - (appState.currentSubText - ? { - text: appState.currentSubText, - tokens: null, - startTime: appState.mpvClient?.currentSubStart ?? null, - endTime: appState.mpvClient?.currentSubEnd ?? null, - } - : null), () => ({ - enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, - mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, - })); - }, - startTexthooker: (port, websocketUrl) => { - if (!texthookerService.isRunning()) { - texthookerService.start(port, websocketUrl); - } - }, - log: (message) => appLogger.logInfo(message), - createMecabTokenizerAndCheck: async () => { - await createMecabTokenizerAndCheck(); - }, - createSubtitleTimingTracker: () => { - const tracker = new subtitle_timing_tracker_1.SubtitleTimingTracker(); - appState.subtitleTimingTracker = tracker; - }, - loadYomitanExtension: async () => { - await loadYomitanExtension(); - }, - ensureYomitanExtensionLoaded: async () => { - await ensureYomitanExtensionLoaded(); - }, - handleFirstRunSetup: async () => { - const snapshot = await firstRunSetupService.ensureSetupStateInitialized(); - appState.firstRunSetupCompleted = snapshot.state.status === 'completed'; - const args = appState.initialArgs; - if (args && (0, first_run_setup_service_1.shouldAutoOpenFirstRunSetup)(args)) { - const force = Boolean(args.setup); - if (force || snapshot.state.status !== 'completed') { - openFirstRunSetupWindow(force); - } - } - }, - startJellyfinRemoteSession: async () => { - await startJellyfinRemoteSession(); - }, - prewarmSubtitleDictionaries: async () => { - await prewarmSubtitleDictionaries(); - }, - startBackgroundWarmups: () => { - startBackgroundWarmupsIfAllowed(); - }, - texthookerOnlyMode: appState.texthookerOnlyMode, - shouldAutoInitializeOverlayRuntimeFromConfig: () => appState.backgroundMode - ? false - : configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(), - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - initializeOverlayRuntime: () => initializeOverlayRuntime(), - runHeadlessInitialCommand: () => runHeadlessInitialCommand(), - handleInitialArgs: () => handleInitialArgs(), - shouldRunHeadlessInitialCommand: () => Boolean(appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)), - shouldUseMinimalStartup: () => (0, startup_mode_flags_1.getStartupModeFlags)(appState.initialArgs).shouldUseMinimalStartup, - shouldSkipHeavyStartup: () => (0, startup_mode_flags_1.getStartupModeFlags)(appState.initialArgs).shouldSkipHeavyStartup, - createImmersionTracker: () => { - ensureImmersionTrackerStarted(); - }, - logDebug: (message) => { - logger.debug(message); - }, - now: () => Date.now(), - }, - immersionTrackerStartupMainDeps, -}); -function ensureOverlayStartupPrereqs() { - if (appState.subtitlePosition === null) { - loadSubtitlePosition(); - } - if (appState.keybindings.length === 0) { - appState.keybindings = (0, utils_2.resolveKeybindings)(getResolvedConfig(), config_2.DEFAULT_KEYBINDINGS); - refreshCurrentSessionBindings(); - } - else if (!appState.sessionBindingsInitialized) { - refreshCurrentSessionBindings(); - } - if (!appState.mpvClient) { - appState.mpvClient = createMpvClientRuntimeService(); - } - if (!appState.runtimeOptionsManager) { - appState.runtimeOptionsManager = new runtime_options_1.RuntimeOptionsManager(() => configService.getConfig().ankiConnect, { - applyAnkiPatch: (patch) => { - if (appState.ankiIntegration) { - appState.ankiIntegration.applyRuntimeConfigPatch(patch); - } - }, - getSubtitleStyleConfig: () => configService.getConfig().subtitleStyle, - onOptionsChanged: () => { - subtitleProcessingController.invalidateTokenizationCache(); - subtitlePrefetchService?.onSeek(lastObservedTimePos); - broadcastRuntimeOptionsChanged(); - refreshOverlayShortcuts(); - }, - }); - } - if (!appState.subtitleTimingTracker) { - appState.subtitleTimingTracker = new subtitle_timing_tracker_1.SubtitleTimingTracker(); - } -} -async function ensureYoutubePlaybackRuntimeReady() { - ensureOverlayStartupPrereqs(); - await ensureYomitanExtensionLoaded(); - if (!appState.overlayRuntimeInitialized) { - initializeOverlayRuntime(); - return; - } - ensureOverlayWindowsReadyForVisibilityActions(); -} -const { createMpvClientRuntimeService: createMpvClientRuntimeServiceHandler, updateMpvSubtitleRenderMetrics: updateMpvSubtitleRenderMetricsHandler, tokenizeSubtitle, createMecabTokenizerAndCheck, prewarmSubtitleDictionaries, startBackgroundWarmups, startTokenizationWarmups, isTokenizationWarmupReady, } = (0, composers_1.composeMpvRuntimeHandlers)({ - bindMpvMainEventHandlersMainDeps: { - appState, - getQuitOnDisconnectArmed: () => jellyfinPlayQuitOnDisconnectArmed || youtubePlaybackRuntime.getQuitOnDisconnectArmed(), - scheduleQuitCheck: (callback) => { - setTimeout(callback, 500); - }, - quitApp: () => requestAppQuit(), - reportJellyfinRemoteStopped: () => { - void reportJellyfinRemoteStopped(); - }, - onMpvConnected: () => { - if (appState.sessionBindingsInitialized) { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, [ - 'script-message', - 'subminer-reload-session-bindings', - ]); - } - if (appState.currentSubText.trim()) { - subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText); - } - }, - maybeRunAnilistPostWatchUpdate: (options) => maybeRunAnilistPostWatchUpdate(options), - recordAnilistMediaDuration: (durationSec) => { - recordAnilistMediaDuration(durationSec); - }, - logSubtitleTimingError: (message, error) => logger.error(message, error), - broadcastToOverlayWindows: (channel, payload) => { - broadcastToOverlayWindows(channel, payload); - }, - getImmediateSubtitlePayload: (text) => subtitleProcessingController.consumeCachedSubtitle(text), - emitImmediateSubtitle: (payload) => { - emitSubtitlePayload(payload); - }, - onSubtitleChange: (text) => { - subtitlePrefetchService?.pause(); - subtitleProcessingController.onSubtitleChange(text); - }, - refreshDiscordPresence: () => { - discordPresenceRuntime.publishDiscordPresence(); - }, - ensureImmersionTrackerInitialized: () => { - ensureImmersionTrackerStarted(); - }, - tokenizeSubtitleForImmersion: async (text) => tokenizeSubtitleDeferred ? await tokenizeSubtitleDeferred(text) : null, - updateCurrentMediaPath: (path) => { - autoplayReadyGate.invalidatePendingAutoplayReadyFallbacks(); - currentMediaTokenizationGate.updateCurrentMediaPath(path); - managedLocalSubtitleSelectionRuntime.handleMediaPathChange(path); - startupOsdSequencer.reset(); - subtitlePrefetchRuntime.clearScheduledSubtitlePrefetchRefresh(); - subtitlePrefetchRuntime.cancelPendingInit(); - youtubePrimarySubtitleNotificationRuntime.handleMediaPathChange(path); - if (path) { - ensureImmersionTrackerStarted(); - // Delay slightly to allow MPV's track-list to be populated. - subtitlePrefetchRuntime.scheduleSubtitlePrefetchRefresh(500); - } - mediaRuntime.updateCurrentMediaPath(path); - }, - restoreMpvSubVisibility: () => { - restoreOverlayMpvSubtitles(); - }, - resetSubtitleSidebarEmbeddedLayout: () => { - resetSubtitleSidebarEmbeddedLayoutRuntime(); - }, - getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(), - resetAnilistMediaTracking: (mediaKey) => { - resetAnilistMediaTracking(mediaKey); - }, - maybeProbeAnilistDuration: (mediaKey) => { - void maybeProbeAnilistDuration(mediaKey); - }, - ensureAnilistMediaGuess: (mediaKey) => { - void ensureAnilistMediaGuess(mediaKey); - }, - syncImmersionMediaState: () => { - immersionMediaRuntime.syncFromCurrentMediaState(); - }, - signalAutoplayReadyIfWarm: () => { - if (!isTokenizationWarmupReady()) { - return; - } - autoplayReadyGate.maybeSignalPluginAutoplayReady({ text: '__warm__', tokens: null }, { forceWhilePaused: true }); - }, - scheduleCharacterDictionarySync: () => { - if (!yomitanProfilePolicy.isCharacterDictionaryEnabled() || isYoutubePlaybackActiveNow()) { - return; - } - characterDictionaryAutoSyncRuntime.scheduleSync(); - }, - updateCurrentMediaTitle: (title) => { - mediaRuntime.updateCurrentMediaTitle(title); - }, - resetAnilistMediaGuessState: () => { - resetAnilistMediaGuessState(); - }, - reportJellyfinRemoteProgress: (forceImmediate) => { - void reportJellyfinRemoteProgress(forceImmediate); - }, - onTimePosUpdate: (time) => { - const delta = time - lastObservedTimePos; - if (subtitlePrefetchService && (delta > SEEK_THRESHOLD_SECONDS || delta < 0)) { - subtitlePrefetchService.onSeek(time); - } - lastObservedTimePos = time; - }, - onFullscreenChange: (fullscreen) => { - cancelLinuxMpvFullscreenOverlayRefreshBurst = (0, linux_mpv_fullscreen_overlay_refresh_1.updateLinuxMpvFullscreenOverlayRefreshBurst)(fullscreen, { - overlayManager: { - getMainWindow: () => overlayManager.getMainWindow(), - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - }, - overlayVisibilityRuntime, - ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), - }, cancelLinuxMpvFullscreenOverlayRefreshBurst); - }, - onSubtitleTrackChange: (sid) => { - scheduleSubtitlePrefetchRefresh(); - youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackChange(sid); - }, - onSubtitleTrackListChange: (trackList) => { - managedLocalSubtitleSelectionRuntime.handleSubtitleTrackListChange(trackList); - scheduleSubtitlePrefetchRefresh(); - youtubePrimarySubtitleNotificationRuntime.handleSubtitleTrackListChange(trackList); - }, - updateSubtitleRenderMetrics: (patch) => { - updateMpvSubtitleRenderMetrics(patch); - }, - syncOverlayMpvSubtitleSuppression: () => { - syncOverlayMpvSubtitleSuppression(); - }, - }, - mpvClientRuntimeServiceFactoryMainDeps: { - createClient: services_1.MpvIpcClient, - getSocketPath: () => appState.mpvSocketPath, - getResolvedConfig: () => getResolvedConfig(), - isAutoStartOverlayEnabled: () => appState.autoStartOverlay, - setOverlayVisible: (visible) => setOverlayVisible(visible), - isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - getReconnectTimer: () => appState.reconnectTimer, - setReconnectTimer: (timer) => { - appState.reconnectTimer = timer; - }, - shouldQuitOnMpvShutdown: () => appState.initialArgs?.managedPlayback === true, - requestAppQuit: () => requestAppQuit(), - }, - updateMpvSubtitleRenderMetricsMainDeps: { - getCurrentMetrics: () => appState.mpvSubtitleRenderMetrics, - setCurrentMetrics: (metrics) => { - appState.mpvSubtitleRenderMetrics = metrics; - }, - applyPatch: (current, patch) => (0, services_1.applyMpvSubtitleRenderMetricsPatch)(current, patch), - broadcastMetrics: () => { - // no renderer consumer for subtitle render metrics updates at present - }, - }, - tokenizer: { - buildTokenizerDepsMainDeps: { - getYomitanExt: () => appState.yomitanExt, - getYomitanSession: () => appState.yomitanSession, - getYomitanParserWindow: () => appState.yomitanParserWindow, - setYomitanParserWindow: (window) => { - appState.yomitanParserWindow = window; - }, - getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise, - setYomitanParserReadyPromise: (promise) => { - appState.yomitanParserReadyPromise = promise; - }, - getYomitanParserInitPromise: () => appState.yomitanParserInitPromise, - setYomitanParserInitPromise: (promise) => { - appState.yomitanParserInitPromise = promise; - }, - isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)), - recordLookup: (hit) => { - ensureImmersionTrackerStarted(); - appState.immersionTracker?.recordLookup(hit); - }, - getKnownWordMatchMode: () => appState.ankiIntegration?.getKnownWordMatchMode() ?? - getResolvedConfig().ankiConnect.knownWords.matchMode, - getNPlusOneEnabled: () => getRuntimeBooleanOption('subtitle.annotation.nPlusOne', getResolvedConfig().ankiConnect.nPlusOne.enabled), - getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords, - getJlptLevel: (text) => appState.jlptLevelLookup(text), - getJlptEnabled: () => getRuntimeBooleanOption('subtitle.annotation.jlpt', getResolvedConfig().subtitleStyle.enableJlpt), - getCharacterDictionaryEnabled: () => getResolvedConfig().anilist.characterDictionary.enabled && - yomitanProfilePolicy.isCharacterDictionaryEnabled() && - !isYoutubePlaybackActiveNow(), - getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled, - getFrequencyDictionaryEnabled: () => getRuntimeBooleanOption('subtitle.annotation.frequency', getResolvedConfig().subtitleStyle.frequencyDictionary.enabled), - getFrequencyDictionaryMatchMode: () => getResolvedConfig().subtitleStyle.frequencyDictionary.matchMode, - getFrequencyRank: (text) => appState.frequencyRankLookup(text), - getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled, - getMecabTokenizer: () => appState.mecabTokenizer, - onTokenizationReady: (text) => { - currentMediaTokenizationGate.markReady(appState.currentMediaPath?.trim() || appState.mpvClient?.currentVideoPath?.trim() || null); - startupOsdSequencer.markTokenizationReady(); - autoplayReadyGate.maybeSignalPluginAutoplayReady({ text, tokens: null }, { forceWhilePaused: true }); - }, - }, - createTokenizerRuntimeDeps: (deps) => (0, services_1.createTokenizerDepsRuntime)(deps), - tokenizeSubtitle: (text, deps) => (0, services_1.tokenizeSubtitle)(text, deps), - createMecabTokenizerAndCheckMainDeps: { - getMecabTokenizer: () => appState.mecabTokenizer, - setMecabTokenizer: (tokenizer) => { - appState.mecabTokenizer = tokenizer; - }, - createMecabTokenizer: () => new mecab_tokenizer_1.MecabTokenizer(), - checkAvailability: async (tokenizer) => tokenizer.checkAvailability(), - }, - prewarmSubtitleDictionariesMainDeps: { - ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(), - ensureFrequencyDictionaryLookup: () => frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(), - showMpvOsd: (message) => showMpvOsd(message), - showLoadingOsd: (message) => startupOsdSequencer.showAnnotationLoading(message), - showLoadedOsd: (message) => startupOsdSequencer.markAnnotationLoadingComplete(message), - shouldShowOsdNotification: () => { - const type = getResolvedConfig().ankiConnect.behavior.notificationType; - return type === 'osd' || type === 'both'; - }, - }, - }, - warmups: { - launchBackgroundWarmupTaskMainDeps: { - now: () => Date.now(), - logDebug: (message) => logger.debug(message), - logWarn: (message) => logger.warn(message), - }, - startBackgroundWarmupsMainDeps: { - getStarted: () => backgroundWarmupsStarted, - setStarted: (started) => { - backgroundWarmupsStarted = started; - }, - isTexthookerOnlyMode: () => appState.texthookerOnlyMode, - ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded().then(() => { }), - shouldWarmupMecab: () => { - const startupWarmups = getResolvedConfig().startupWarmups; - if (startupWarmups.lowPowerMode) { - return false; - } - if (!startupWarmups.mecab) { - return false; - } - return shouldInitializeMecabForAnnotations(); - }, - shouldWarmupYomitanExtension: () => getResolvedConfig().startupWarmups.yomitanExtension, - shouldWarmupSubtitleDictionaries: () => { - const startupWarmups = getResolvedConfig().startupWarmups; - if (startupWarmups.lowPowerMode) { - return false; - } - return startupWarmups.subtitleDictionaries; - }, - shouldWarmupJellyfinRemoteSession: () => { - const startupWarmups = getResolvedConfig().startupWarmups; - if (startupWarmups.lowPowerMode) { - return false; - } - return startupWarmups.jellyfinRemoteSession; - }, - shouldAutoConnectJellyfinRemote: () => { - const jellyfin = getResolvedConfig().jellyfin; - return (jellyfin.enabled && jellyfin.remoteControlEnabled && jellyfin.remoteControlAutoConnect); - }, - startJellyfinRemoteSession: () => startJellyfinRemoteSession(), - logDebug: (message) => logger.debug(message), - }, - }, -}); -tokenizeSubtitleDeferred = tokenizeSubtitle; -function createMpvClientRuntimeService() { - const client = createMpvClientRuntimeServiceHandler(); - client.on('connection-change', ({ connected }) => { - if (connected) { - return; - } - if (!youtubeFlowRuntime.hasActiveSession()) { - return; - } - youtubeFlowRuntime.cancelActivePicker(); - broadcastToOverlayWindows(contracts_1.IPC_CHANNELS.event.youtubePickerCancel, null); - overlayModalRuntime.handleOverlayModalClosed('youtube-track-picker'); - }); - return client; -} -function resetSubtitleSidebarEmbeddedLayoutRuntime() { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['set_property', 'video-margin-ratio-right', 0]); - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['set_property', 'video-pan-x', 0]); -} -function updateMpvSubtitleRenderMetrics(patch) { - updateMpvSubtitleRenderMetricsHandler(patch); -} -let lastOverlayWindowGeometry = null; -function getOverlayGeometryFallback() { - const cursorPoint = electron_1.screen.getCursorScreenPoint(); - const display = electron_1.screen.getDisplayNearestPoint(cursorPoint); - const bounds = display.workArea; - return { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }; -} -function getCurrentOverlayGeometry() { - if (lastOverlayWindowGeometry) - return lastOverlayWindowGeometry; - const trackerGeometry = appState.windowTracker?.getGeometry(); - if (trackerGeometry) - return trackerGeometry; - return getOverlayGeometryFallback(); -} -function geometryMatches(a, b) { - if (!a || !b) - return false; - return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; -} -function applyOverlayRegions(geometry) { - lastOverlayWindowGeometry = geometry; - overlayManager.setOverlayWindowBounds(geometry); - overlayManager.setModalWindowBounds(geometry); -} -const buildUpdateVisibleOverlayBoundsMainDepsHandler = (0, overlay_1.createBuildUpdateVisibleOverlayBoundsMainDepsHandler)({ - setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry), - afterSetOverlayWindowBounds: () => { - if (!overlayManager.getVisibleOverlayVisible()) { - return; - } - if (process.platform === 'win32') { - scheduleWindowsVisibleOverlayZOrderSyncBurst(); - return; - } - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - return; - } - ensureOverlayWindowLevel(mainWindow); - }, -}); -const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler(); -const updateVisibleOverlayBounds = (0, overlay_1.createUpdateVisibleOverlayBoundsHandler)(updateVisibleOverlayBoundsMainDeps); -const buildEnsureOverlayWindowLevelMainDepsHandler = (0, overlay_1.createBuildEnsureOverlayWindowLevelMainDepsHandler)({ - ensureOverlayWindowLevelCore: (window) => (0, services_1.ensureOverlayWindowLevel)(window), -}); -const ensureOverlayWindowLevelMainDeps = buildEnsureOverlayWindowLevelMainDepsHandler(); -const ensureOverlayWindowLevel = (0, overlay_1.createEnsureOverlayWindowLevelHandler)(ensureOverlayWindowLevelMainDeps); -function syncPrimaryOverlayWindowLayer(layer) { - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) - return; - (0, services_1.syncOverlayWindowLayer)(mainWindow, layer); -} -const buildEnforceOverlayLayerOrderMainDepsHandler = (0, overlay_1.createBuildEnforceOverlayLayerOrderMainDepsHandler)({ - enforceOverlayLayerOrderCore: (params) => (0, services_1.enforceOverlayLayerOrder)({ - visibleOverlayVisible: params.visibleOverlayVisible, - mainWindow: params.mainWindow, - ensureOverlayWindowLevel: (window) => params.ensureOverlayWindowLevel(window), - }), - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - getMainWindow: () => overlayManager.getMainWindow(), - ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), -}); -const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler(); -const enforceOverlayLayerOrder = (0, overlay_1.createEnforceOverlayLayerOrderHandler)(enforceOverlayLayerOrderMainDeps); -async function loadYomitanExtension() { - const extension = await yomitanExtensionRuntime.loadYomitanExtension(); - if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) { - await syncYomitanDefaultProfileAnkiServer(); - } - return extension; -} -async function ensureYomitanExtensionLoaded() { - const extension = await yomitanExtensionRuntime.ensureYomitanExtensionLoaded(); - if (extension && !yomitanProfilePolicy.isExternalReadOnlyMode()) { - await syncYomitanDefaultProfileAnkiServer(); - } - return extension; -} -let lastSyncedYomitanAnkiServer = null; -function getPreferredYomitanAnkiServerUrl() { - return (0, yomitan_anki_server_1.getPreferredYomitanAnkiServerUrl)(getResolvedConfig().ankiConnect); -} -function getYomitanParserRuntimeDeps() { - return { - getYomitanExt: () => appState.yomitanExt, - getYomitanSession: () => appState.yomitanSession, - getYomitanParserWindow: () => appState.yomitanParserWindow, - setYomitanParserWindow: (window) => { - appState.yomitanParserWindow = window; - }, - getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise, - setYomitanParserReadyPromise: (promise) => { - appState.yomitanParserReadyPromise = promise; - }, - getYomitanParserInitPromise: () => appState.yomitanParserInitPromise, - setYomitanParserInitPromise: (promise) => { - appState.yomitanParserInitPromise = promise; - }, - }; -} -async function syncYomitanDefaultProfileAnkiServer() { - if (yomitanProfilePolicy.isExternalReadOnlyMode()) { - return; - } - const targetUrl = getPreferredYomitanAnkiServerUrl().trim(); - if (!targetUrl || targetUrl === lastSyncedYomitanAnkiServer) { - return; - } - const synced = await (0, services_1.syncYomitanDefaultAnkiServer)(targetUrl, getYomitanParserRuntimeDeps(), { - error: (message, ...args) => { - logger.error(message, ...args); - }, - info: (message, ...args) => { - logger.info(message, ...args); - }, - }, { - forceOverride: (0, yomitan_anki_server_1.shouldForceOverrideYomitanAnkiServer)(getResolvedConfig().ankiConnect), - }); - if (synced) { - lastSyncedYomitanAnkiServer = targetUrl; - } -} -function createModalWindow() { - const existingWindow = overlayManager.getModalWindow(); - if (existingWindow && !existingWindow.isDestroyed()) { - return existingWindow; - } - const window = createModalWindowHandler(); - overlayManager.setModalWindowBounds(getCurrentOverlayGeometry()); - return window; -} -function createMainWindow() { - const window = createMainWindowHandler(); - if (process.platform === 'win32') { - const overlayHwnd = getWindowsNativeWindowHandleNumber(window); - if (!(0, windows_helper_1.ensureWindowsOverlayTransparency)(overlayHwnd)) { - logger.warn('Failed to eagerly extend Windows overlay transparency via koffi'); - } - } - return window; -} -function ensureTray() { - ensureTrayHandler(); -} -function destroyTray() { - destroyTrayHandler(); -} -function initializeOverlayRuntime() { - initializeOverlayRuntimeHandler(); - appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined); - appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAfterKnownWordUpdate); - syncOverlayMpvSubtitleSuppression(); -} -function openYomitanSettings() { - if (yomitanProfilePolicy.isExternalReadOnlyMode()) { - const message = 'Yomitan settings unavailable while using read-only external-profile mode.'; - logger.warn('Yomitan settings window disabled while yomitan.externalProfilePath is configured because external profile mode is read-only.'); - (0, utils_2.showDesktopNotification)('SubMiner', { body: message }); - showMpvOsd(message); - return false; - } - openYomitanSettingsHandler(); - return true; -} -const { getConfiguredShortcuts, registerGlobalShortcuts, refreshGlobalAndOverlayShortcuts, cancelPendingMultiCopy, startPendingMultiCopy, cancelPendingMineSentenceMultiple, startPendingMineSentenceMultiple, syncOverlayShortcuts, refreshOverlayShortcuts, } = (0, composers_1.composeShortcutRuntimes)({ - globalShortcuts: { - getConfiguredShortcutsMainDeps: { - getResolvedConfig: () => getResolvedConfig(), - defaultConfig: config_2.DEFAULT_CONFIG, - resolveConfiguredShortcuts: utils_2.resolveConfiguredShortcuts, - }, - buildRegisterGlobalShortcutsMainDeps: (getConfiguredShortcutsHandler) => ({ - getConfiguredShortcuts: () => getConfiguredShortcutsHandler(), - registerGlobalShortcutsCore: services_1.registerGlobalShortcuts, - toggleVisibleOverlay: () => toggleVisibleOverlay(), - openYomitanSettings: () => openYomitanSettings(), - isDev, - getMainWindow: () => overlayManager.getMainWindow(), - }), - buildRefreshGlobalAndOverlayShortcutsMainDeps: (registerGlobalShortcutsHandler) => ({ - unregisterAllGlobalShortcuts: () => electron_1.globalShortcut.unregisterAll(), - registerGlobalShortcuts: () => registerGlobalShortcutsHandler(), - syncOverlayShortcuts: () => syncOverlayShortcuts(), - }), - }, - numericShortcutRuntimeMainDeps: { - globalShortcut: electron_1.globalShortcut, - showMpvOsd: (text) => showMpvOsd(text), - setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs), - clearTimer: (timer) => clearTimeout(timer), - }, - numericSessions: { - onMultiCopyDigit: (count) => handleMultiCopyDigit(count), - onMineSentenceDigit: (count) => handleMineSentenceDigit(count), - }, - overlayShortcutsRuntimeMainDeps: { - overlayShortcutsRuntime, - }, -}); -function resolveSessionBindingPlatform() { - if (process.platform === 'darwin') - return 'darwin'; - if (process.platform === 'win32') - return 'win32'; - return 'linux'; -} -function compileCurrentSessionBindings() { - return (0, session_bindings_1.compileSessionBindings)({ - keybindings: appState.keybindings, - shortcuts: getConfiguredShortcuts(), - statsToggleKey: getResolvedConfig().stats.toggleKey, - platform: resolveSessionBindingPlatform(), - rawConfig: getResolvedConfig(), - }); -} -function persistSessionBindings(bindings, warnings = []) { - const artifact = (0, session_bindings_1.buildPluginSessionBindingsArtifact)({ - bindings, - warnings, - numericSelectionTimeoutMs: getConfiguredShortcuts().multiCopyTimeoutMs, - }); - (0, session_bindings_artifact_1.writeSessionBindingsArtifact)(CONFIG_DIR, artifact); - appState.sessionBindings = bindings; - appState.sessionBindingsInitialized = true; - if (appState.mpvClient?.connected) { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, [ - 'script-message', - 'subminer-reload-session-bindings', - ]); - } -} -function refreshCurrentSessionBindings() { - const compiled = compileCurrentSessionBindings(); - for (const warning of compiled.warnings) { - logger.warn(`[session-bindings] ${warning.message}`); - } - persistSessionBindings(compiled.bindings, compiled.warnings); -} -const { flushMpvLog, showMpvOsd } = (0, mpv_1.createMpvOsdRuntimeHandlers)({ - appendToMpvLogMainDeps: { - logPath: DEFAULT_MPV_LOG_PATH, - dirname: (targetPath) => path.dirname(targetPath), - mkdir: async (targetPath, options) => { - await fs.promises.mkdir(targetPath, options); - }, - appendFile: async (targetPath, data, options) => { - await fs.promises.appendFile(targetPath, data, options); - }, - now: () => new Date(), - }, - buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({ - appendToMpvLog: (message) => appendToMpvLogHandler(message), - showMpvOsdRuntime: (mpvClient, text, fallbackLog) => (0, services_1.showMpvOsdRuntime)(mpvClient, text, fallbackLog), - getMpvClient: () => appState.mpvClient, - logInfo: (line) => logger.info(line), - }), -}); -flushPendingMpvLogWrites = () => { - void flushMpvLog(); -}; -const updateStateStore = (0, update_service_1.createFileUpdateStateStore)(path.join(USER_DATA_PATH, 'update-state.json')); -let updateService = null; -const electronNetFetch = (0, fetch_adapter_1.createElectronNetFetch)({ - fetch: (url, init) => electron_1.net.fetch(url, init), -}); -function getFetchForUpdater() { - return electronNetFetch; -} -async function updateLauncherFromSelectedRelease(launcherPath, channel = getResolvedConfig().updates.channel, release = null) { - const fetchForUpdater = getFetchForUpdater(); - if (!release) { - return { status: 'missing-asset', message: `No ${channel} GitHub release found.` }; - } - const sumsAsset = (0, release_assets_1.findReleaseAsset)(release, 'SHA256SUMS.txt'); - if (!sumsAsset) { - return { status: 'missing-asset', message: 'Release has no SHA256SUMS.txt asset.' }; - } - const sums = (0, release_assets_1.parseSha256Sums)(await (0, release_assets_1.fetchReleaseAssetText)(fetchForUpdater, sumsAsset.browser_download_url)); - const launcherResult = await (0, launcher_updater_1.updateLauncherFromRelease)({ - release, - sha256Sums: sums, - launcherPath, - downloadAsset: (url) => (0, release_assets_1.fetchReleaseAssetBuffer)(fetchForUpdater, url), - }); - const supportResults = await (0, support_assets_1.updateSupportAssetsFromRelease)({ - release, - sha256Sums: sums, - downloadAsset: (url) => (0, release_assets_1.fetchReleaseAssetBuffer)(fetchForUpdater, url), - }); - for (const result of supportResults) { - if (result.status === 'protected' && result.command) { - logger.warn(`Rofi theme update requires manual command: ${result.command}`); - } - else if (result.status === 'hash-mismatch' || result.status === 'missing-asset') { - logger.warn(`Rofi theme update skipped: ${result.message ?? result.status}`); - } - } - return launcherResult; -} -function getUpdateService() { - if (updateService) - return updateService; - const appUpdater = (0, app_updater_1.createElectronAppUpdater)({ - currentVersion: electron_1.app.getVersion(), - isPackaged: electron_1.app.isPackaged, - log: (message) => logger.info(message), - getChannel: () => getResolvedConfig().updates.channel, - configureHttpExecutor: process.platform === 'darwin' ? () => (0, curl_http_executor_1.createCurlHttpExecutor)() : undefined, - disableDifferentialDownload: process.platform === 'darwin', - isNativeUpdaterSupported: () => (0, app_updater_1.isNativeUpdaterSupported)({ - platform: process.platform, - isPackaged: electron_1.app.isPackaged, - execPath: process.execPath, - env: process.env, - log: (message) => logger.warn(message), - }), - }); - const updateDialogPresenter = (0, update_dialogs_1.createUpdateDialogPresenter)({ - platform: process.platform, - focusApp: () => electron_1.app.focus({ steal: true }), - showMessageBox: (options) => electron_1.dialog.showMessageBox(options), - }); - updateService = (0, update_service_1.createUpdateService)({ - getConfig: () => getResolvedConfig().updates, - getCurrentVersion: () => electron_1.app.getVersion(), - now: () => Date.now(), - readState: () => updateStateStore.readState(), - writeState: (state) => updateStateStore.writeState(state), - checkAppUpdate: (channel) => appUpdater.checkForUpdates(channel), - shouldFetchReleaseMetadata: ({ appUpdate }) => (0, release_metadata_policy_1.shouldFetchReleaseMetadataForPlatform)(process.platform, appUpdate), - fetchLatestStableRelease: (channel) => (0, release_assets_1.fetchLatestStableRelease)({ fetch: getFetchForUpdater(), channel }), - updateLauncher: (launcherPath, channel, release) => updateLauncherFromSelectedRelease(launcherPath, channel, release), - showNoUpdateDialog: (version) => updateDialogPresenter.showNoUpdateDialog(version), - showUpdateAvailableDialog: (version) => updateDialogPresenter.showUpdateAvailableDialog(version), - showUpdateFailedDialog: (message) => updateDialogPresenter.showUpdateFailedDialog(message), - showManualUpdateRequiredDialog: (version) => updateDialogPresenter.showManualUpdateRequiredDialog(version), - downloadAppUpdate: () => appUpdater.downloadUpdate(), - showRestartDialog: () => updateDialogPresenter.showRestartDialog(), - quitAndInstall: () => appUpdater.quitAndInstall(), - notifyUpdateAvailable: (version) => (0, update_notifications_1.notifyUpdateAvailable)({ notificationType: getResolvedConfig().updates.notificationType, version }, { - showSystemNotification: (title, body) => (0, utils_2.showDesktopNotification)(title, { body }), - showOsdNotification: (message) => showMpvOsd(message), - log: (message) => logger.warn(message), - }), - log: (message) => logger.warn(message), - }); - return updateService; -} -const cycleSecondarySubMode = (0, mpv_1.createCycleSecondarySubModeRuntimeHandler)({ - cycleSecondarySubModeMainDeps: { - getSecondarySubMode: () => appState.secondarySubMode, - setSecondarySubMode: (mode) => { - setSecondarySubMode(mode); - }, - getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs, - setLastSecondarySubToggleAtMs: (timestampMs) => { - appState.lastSecondarySubToggleAtMs = timestampMs; - }, - broadcastToOverlayWindows: (channel, mode) => { - broadcastToOverlayWindows(channel, mode); - }, - showMpvOsd: (text) => showMpvOsd(text), - }, - cycleSecondarySubMode: (deps) => (0, services_1.cycleSecondarySubMode)(deps), -}); -function setSecondarySubMode(mode) { - appState.secondarySubMode = mode; -} -function handleCycleSecondarySubMode() { - cycleSecondarySubMode(); -} -function toggleSubtitleSidebar() { - broadcastToOverlayWindows(contracts_1.IPC_CHANNELS.event.subtitleSidebarToggle); -} -function togglePrimarySubtitleBar() { - broadcastToOverlayWindows(contracts_1.IPC_CHANNELS.event.primarySubtitleBarToggle); -} -async function triggerSubsyncFromConfig() { - await subsyncRuntime.triggerFromConfig(); -} -function handleMultiCopyDigit(count) { - handleMultiCopyDigitHandler(count); -} -function copyCurrentSubtitle() { - copyCurrentSubtitleHandler(); -} -const buildUpdateLastCardFromClipboardMainDepsHandler = (0, mining_1.createBuildUpdateLastCardFromClipboardMainDepsHandler)({ - getAnkiIntegration: () => appState.ankiIntegration, - readClipboardText: () => electron_1.clipboard.readText(), - showMpvOsd: (text) => showMpvOsd(text), - updateLastCardFromClipboardCore: services_1.updateLastCardFromClipboard, -}); -const updateLastCardFromClipboardMainDeps = buildUpdateLastCardFromClipboardMainDepsHandler(); -const updateLastCardFromClipboardHandler = (0, mining_1.createUpdateLastCardFromClipboardHandler)(updateLastCardFromClipboardMainDeps); -const buildRefreshKnownWordCacheMainDepsHandler = (0, mining_1.createBuildRefreshKnownWordCacheMainDepsHandler)({ - getAnkiIntegration: () => appState.ankiIntegration, - missingIntegrationMessage: 'AnkiConnect integration not enabled', -}); -const refreshKnownWordCacheMainDeps = buildRefreshKnownWordCacheMainDepsHandler(); -const refreshKnownWordCacheHandler = (0, mining_1.createRefreshKnownWordCacheHandler)(refreshKnownWordCacheMainDeps); -const buildTriggerFieldGroupingMainDepsHandler = (0, mining_1.createBuildTriggerFieldGroupingMainDepsHandler)({ - getAnkiIntegration: () => appState.ankiIntegration, - showMpvOsd: (text) => showMpvOsd(text), - triggerFieldGroupingCore: services_1.triggerFieldGrouping, -}); -const triggerFieldGroupingMainDeps = buildTriggerFieldGroupingMainDepsHandler(); -const triggerFieldGroupingHandler = (0, mining_1.createTriggerFieldGroupingHandler)(triggerFieldGroupingMainDeps); -const buildMarkLastCardAsAudioCardMainDepsHandler = (0, mining_1.createBuildMarkLastCardAsAudioCardMainDepsHandler)({ - getAnkiIntegration: () => appState.ankiIntegration, - showMpvOsd: (text) => showMpvOsd(text), - markLastCardAsAudioCardCore: services_1.markLastCardAsAudioCard, -}); -const markLastCardAsAudioCardMainDeps = buildMarkLastCardAsAudioCardMainDepsHandler(); -const markLastCardAsAudioCardHandler = (0, mining_1.createMarkLastCardAsAudioCardHandler)(markLastCardAsAudioCardMainDeps); -const buildMineSentenceCardMainDepsHandler = (0, mining_1.createBuildMineSentenceCardMainDepsHandler)({ - getAnkiIntegration: () => appState.ankiIntegration, - getMpvClient: () => appState.mpvClient, - showMpvOsd: (text) => showMpvOsd(text), - mineSentenceCardCore: services_1.mineSentenceCard, - recordCardsMined: (count, noteIds) => { - ensureImmersionTrackerStarted(); - appState.immersionTracker?.recordCardsMined(count, noteIds); - }, -}); -const mineSentenceCardHandler = (0, mining_1.createMineSentenceCardHandler)(buildMineSentenceCardMainDepsHandler()); -const buildHandleMultiCopyDigitMainDepsHandler = (0, mining_1.createBuildHandleMultiCopyDigitMainDepsHandler)({ - getSubtitleTimingTracker: () => appState.subtitleTimingTracker, - writeClipboardText: (text) => electron_1.clipboard.writeText(text), - showMpvOsd: (text) => showMpvOsd(text), - handleMultiCopyDigitCore: services_1.handleMultiCopyDigit, -}); -const handleMultiCopyDigitMainDeps = buildHandleMultiCopyDigitMainDepsHandler(); -const handleMultiCopyDigitHandler = (0, mining_1.createHandleMultiCopyDigitHandler)(handleMultiCopyDigitMainDeps); -const buildCopyCurrentSubtitleMainDepsHandler = (0, mining_1.createBuildCopyCurrentSubtitleMainDepsHandler)({ - getSubtitleTimingTracker: () => appState.subtitleTimingTracker, - writeClipboardText: (text) => electron_1.clipboard.writeText(text), - showMpvOsd: (text) => showMpvOsd(text), - copyCurrentSubtitleCore: services_1.copyCurrentSubtitle, -}); -const copyCurrentSubtitleMainDeps = buildCopyCurrentSubtitleMainDepsHandler(); -const copyCurrentSubtitleHandler = (0, mining_1.createCopyCurrentSubtitleHandler)(copyCurrentSubtitleMainDeps); -const buildHandleMineSentenceDigitMainDepsHandler = (0, mining_1.createBuildHandleMineSentenceDigitMainDepsHandler)({ - getSubtitleTimingTracker: () => appState.subtitleTimingTracker, - getAnkiIntegration: () => appState.ankiIntegration, - getCurrentSecondarySubText: () => appState.mpvClient?.currentSecondarySubText || undefined, - showMpvOsd: (text) => showMpvOsd(text), - logError: (message, err) => { - logger.error(message, err); - }, - onCardsMined: (cards) => { - ensureImmersionTrackerStarted(); - appState.immersionTracker?.recordCardsMined(cards); - }, - handleMineSentenceDigitCore: services_1.handleMineSentenceDigit, -}); -const handleMineSentenceDigitMainDeps = buildHandleMineSentenceDigitMainDepsHandler(); -const handleMineSentenceDigitHandler = (0, mining_1.createHandleMineSentenceDigitHandler)(handleMineSentenceDigitMainDeps); -const { setVisibleOverlayVisible: setVisibleOverlayVisibleHandler, toggleVisibleOverlay: toggleVisibleOverlayHandler, setOverlayVisible: setOverlayVisibleHandler, } = (0, overlay_1.createOverlayVisibilityRuntime)({ - setVisibleOverlayVisibleDeps: { - setVisibleOverlayVisibleCore: services_1.setVisibleOverlayVisible, - setVisibleOverlayVisibleState: (nextVisible) => { - overlayManager.setVisibleOverlayVisible(nextVisible); - }, - updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(), - }, - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), -}); -const buildHandleOverlayModalClosedMainDepsHandler = (0, overlay_1.createBuildHandleOverlayModalClosedMainDepsHandler)({ - handleOverlayModalClosedRuntime: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal), -}); -const handleOverlayModalClosedMainDeps = buildHandleOverlayModalClosedMainDepsHandler(); -const handleOverlayModalClosedHandler = (0, overlay_1.createHandleOverlayModalClosedHandler)(handleOverlayModalClosedMainDeps); -const buildAppendClipboardVideoToQueueMainDepsHandler = (0, overlay_1.createBuildAppendClipboardVideoToQueueMainDepsHandler)({ - appendClipboardVideoToQueueRuntime: startup_1.appendClipboardVideoToQueueRuntime, - getMpvClient: () => appState.mpvClient, - readClipboardText: () => electron_1.clipboard.readText(), - showMpvOsd: (text) => showMpvOsd(text), - sendMpvCommand: (command) => { - (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command); - }, -}); -const appendClipboardVideoToQueueMainDeps = buildAppendClipboardVideoToQueueMainDepsHandler(); -const appendClipboardVideoToQueueHandler = (0, overlay_1.createAppendClipboardVideoToQueueHandler)(appendClipboardVideoToQueueMainDeps); -async function loadSubtitleSourceText(source) { - if (/^https?:\/\//i.test(source)) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 4000); - try { - const response = await fetch(source, { signal: controller.signal }); - if (!response.ok) { - throw new Error(`Failed to download subtitle source (${response.status})`); - } - return await response.text(); - } - finally { - clearTimeout(timeoutId); - } - } - const filePath = (0, subtitle_prefetch_source_1.resolveSubtitleSourcePath)(source); - return fs.promises.readFile(filePath, 'utf8'); -} -function parseTrackId(value) { - if (typeof value === 'number' && Number.isInteger(value)) { - return value; - } - if (typeof value === 'string') { - const parsed = Number(value.trim()); - return Number.isInteger(parsed) ? parsed : null; - } - return null; -} -function buildFfmpegSubtitleExtractionArgs(videoPath, ffIndex, outputPath) { - return [ - '-hide_banner', - '-nostdin', - '-y', - '-loglevel', - 'error', - '-an', - '-vn', - '-i', - videoPath, - '-map', - `0:${ffIndex}`, - '-f', - path.extname(outputPath).slice(1), - outputPath, - ]; -} -async function extractInternalSubtitleTrackToTempFile(ffmpegPath, videoPath, track) { - const ffIndex = parseTrackId(track['ff-index']); - const codec = typeof track.codec === 'string' ? track.codec : null; - const extension = (0, utils_3.codecToExtension)(codec ?? undefined); - if (ffIndex === null || extension === null) { - return null; - } - const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'subminer-sidebar-')); - const outputPath = path.join(tempDir, `track_${ffIndex}.${extension}`); - try { - await new Promise((resolve, reject) => { - const child = (0, node_child_process_1.spawn)(ffmpegPath, buildFfmpegSubtitleExtractionArgs(videoPath, ffIndex, outputPath)); - let stderr = ''; - child.stderr.on('data', (chunk) => { - stderr += chunk.toString(); - }); - child.on('error', (error) => { - reject(error); - }); - child.on('close', (code) => { - if (code === 0) { - resolve(); - return; - } - reject(new Error(stderr.trim() || `ffmpeg exited with code ${code ?? 'unknown'}`)); - }); - }); - } - catch (error) { - await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => undefined); - throw error; - } - return { - path: outputPath, - cleanup: async () => { - await fs.promises.rm(tempDir, { recursive: true, force: true }); - }, - }; -} -const shiftSubtitleDelayToAdjacentCueHandler = (0, services_1.createShiftSubtitleDelayToAdjacentCueHandler)({ - getMpvClient: () => appState.mpvClient, - loadSubtitleSourceText, - sendMpvCommand: (command) => (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, command), - showMpvOsd: (text) => showMpvOsd(text), -}); -async function dispatchSessionAction(request) { - await (0, session_actions_1.dispatchSessionAction)(request, { - toggleStatsOverlay: () => (0, stats_window_js_2.toggleStatsOverlay)({ - staticDir: statsDistPath, - preloadPath: statsPreloadPath, - getApiBaseUrl: () => ensureStatsServerStarted().url, - getToggleKey: () => getResolvedConfig().stats.toggleKey, - resolveBounds: () => getCurrentOverlayGeometry(), - onVisibilityChanged: (visible) => { - appState.statsOverlayVisible = visible; - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - }, - }), - toggleVisibleOverlay: () => toggleVisibleOverlay(), - copyCurrentSubtitle: () => copyCurrentSubtitle(), - copySubtitleCount: (count) => handleMultiCopyDigit(count), - updateLastCardFromClipboard: () => updateLastCardFromClipboard(), - triggerFieldGrouping: () => triggerFieldGrouping(), - triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), - mineSentenceCard: () => mineSentenceCard(), - mineSentenceCount: (count) => handleMineSentenceDigit(count), - toggleSecondarySub: () => handleCycleSecondarySubMode(), - toggleSubtitleSidebar: () => toggleSubtitleSidebar(), - markLastCardAsAudioCard: () => markLastCardAsAudioCard(), - openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), - openJimaku: () => openJimakuOverlay(), - openSessionHelp: () => openSessionHelpOverlay(), - openCharacterDictionary: () => openCharacterDictionaryOverlay(), - openControllerSelect: () => openControllerSelectOverlay(), - openControllerDebug: () => openControllerDebugOverlay(), - openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(), - openPlaylistBrowser: () => openPlaylistBrowser(), - replayCurrentSubtitle: () => (0, services_1.replayCurrentSubtitleRuntime)(appState.mpvClient), - playNextSubtitle: () => (0, services_1.playNextSubtitleRuntime)(appState.mpvClient), - shiftSubDelayToAdjacentSubtitle: (direction) => shiftSubtitleDelayToAdjacentCueHandler(direction), - cycleRuntimeOption: (id, direction) => { - if (!appState.runtimeOptionsManager) { - return { ok: false, error: 'Runtime options manager unavailable' }; - } - return (0, runtime_options_ipc_1.applyRuntimeOptionResultRuntime)(appState.runtimeOptionsManager.cycleOption(id, direction), (text) => showMpvOsd(text)); - }, - showMpvOsd: (text) => showMpvOsd(text), - }); -} -const { playlistBrowserMainDeps } = (0, playlist_browser_ipc_1.createPlaylistBrowserIpcRuntime)(() => appState.mpvClient, { - getPrimarySubtitleLanguages: () => getResolvedConfig().youtube.primarySubLanguages, - getSecondarySubtitleLanguages: () => getResolvedConfig().secondarySub.secondarySubLanguages, -}); -const { registerIpcRuntimeHandlers } = (0, composers_1.composeIpcRuntimeHandlers)({ - mpvCommandMainDeps: { - triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), - openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), - openJimaku: () => openJimakuOverlay(), - openYoutubeTrackPicker: () => openYoutubeTrackPickerFromPlayback(), - openPlaylistBrowser: () => openPlaylistBrowser(), - cycleRuntimeOption: (id, direction) => { - if (!appState.runtimeOptionsManager) { - return { ok: false, error: 'Runtime options manager unavailable' }; - } - return (0, runtime_options_ipc_1.applyRuntimeOptionResultRuntime)(appState.runtimeOptionsManager.cycleOption(id, direction), (text) => showMpvOsd(text)); - }, - showMpvOsd: (text) => showMpvOsd(text), - replayCurrentSubtitle: () => (0, services_1.replayCurrentSubtitleRuntime)(appState.mpvClient), - playNextSubtitle: () => (0, services_1.playNextSubtitleRuntime)(appState.mpvClient), - shiftSubDelayToAdjacentSubtitle: (direction) => shiftSubtitleDelayToAdjacentCueHandler(direction), - sendMpvCommand: (rawCommand) => (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, rawCommand), - getMpvClient: () => appState.mpvClient, - isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected), - hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null, - }, - handleMpvCommandFromIpcRuntime: ipc_mpv_command_1.handleMpvCommandFromIpcRuntime, - runSubsyncManualFromIpc: (request) => subsyncRuntime.runManualFromIpc(request), - registration: { - runtimeOptions: { - getRuntimeOptionsManager: () => appState.runtimeOptionsManager, - showMpvOsd: (text) => showMpvOsd(text), - }, - mainDeps: { - getMainWindow: () => overlayManager.getMainWindow(), - getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(), - focusMainWindow: () => { - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) - return; - if (!mainWindow.isFocused()) { - mainWindow.focus(); - } - }, - onOverlayModalClosed: (modal, senderWindow) => { - const modalWindow = overlayManager.getModalWindow(); - if (senderWindow && - modalWindow && - senderWindow === modalWindow && - !senderWindow.isDestroyed()) { - senderWindow.setIgnoreMouseEvents(true, { forward: true }); - senderWindow.hide(); - } - handleOverlayModalClosed(modal); - }, - onOverlayModalOpened: (modal) => { - overlayModalRuntime.notifyOverlayModalOpened(modal); - }, - onOverlayMouseInteractionChanged: (active, senderWindow) => { - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || senderWindow !== mainWindow) { - return; - } - if (visibleOverlayInteractionActive === active) { - if (active && process.platform === 'darwin' && !mainWindow.isFocused()) { - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - } - return; - } - visibleOverlayInteractionActive = active; - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - }, - onYoutubePickerResolve: (request) => youtubeFlowRuntime.resolveActivePicker(request), - openYomitanSettings: () => openYomitanSettings(), - quitApp: () => requestAppQuit(), - toggleVisibleOverlay: () => toggleVisibleOverlay(), - tokenizeCurrentSubtitle: async () => (0, current_subtitle_snapshot_1.resolveCurrentSubtitleForRenderer)({ - currentSubText: appState.currentSubText, - currentSubtitleData: appState.currentSubtitleData, - withCurrentSubtitleTiming: (payload) => withCurrentSubtitleTiming(payload), - }), - getCurrentSubtitleRaw: () => appState.currentSubText, - getCurrentSubtitleAss: () => appState.currentSubAssText, - getSubtitleSidebarSnapshot: async () => { - const currentSubtitle = { - text: appState.currentSubText, - startTime: appState.mpvClient?.currentSubStart ?? null, - endTime: appState.mpvClient?.currentSubEnd ?? null, - }; - const currentTimeSec = appState.mpvClient?.currentTimePos ?? null; - const config = getResolvedConfig().subtitleSidebar; - const client = appState.mpvClient; - if (!client?.connected) { - return { - cues: appState.activeParsedSubtitleCues, - currentTimeSec, - currentSubtitle, - config, - }; - } - try { - const [currentExternalFilenameRaw, currentTrackRaw, trackListRaw, sidRaw, videoPathRaw] = await Promise.all([ - client.requestProperty('current-tracks/sub/external-filename').catch(() => null), - client.requestProperty('current-tracks/sub').catch(() => null), - client.requestProperty('track-list'), - client.requestProperty('sid'), - client.requestProperty('path'), - ]); - const videoPath = typeof videoPathRaw === 'string' ? videoPathRaw : ''; - if (!videoPath) { - return { - cues: appState.activeParsedSubtitleCues, - currentTimeSec, - currentSubtitle, - config, - }; - } - const resolvedSource = await resolveActiveSubtitleSidebarSourceHandler({ - currentExternalFilenameRaw, - currentTrackRaw, - trackListRaw, - sidRaw, - videoPath, - }); - if (!resolvedSource) { - return { - cues: appState.activeParsedSubtitleCues, - currentTimeSec, - currentSubtitle, - config, - }; - } - try { - if (appState.activeParsedSubtitleSource === resolvedSource.sourceKey) { - return { - cues: appState.activeParsedSubtitleCues, - currentTimeSec, - currentSubtitle, - config, - }; - } - const content = await loadSubtitleSourceText(resolvedSource.path); - const cues = (0, subtitle_cue_parser_1.parseSubtitleCues)(content, resolvedSource.path); - appState.activeParsedSubtitleCues = cues; - appState.activeParsedSubtitleSource = resolvedSource.sourceKey; - return { - cues, - currentTimeSec, - currentSubtitle, - config, - }; - } - finally { - await resolvedSource.cleanup?.(); - } - } - catch { - return { - cues: appState.activeParsedSubtitleCues, - currentTimeSec, - currentSubtitle, - config, - }; - } - }, - getPlaybackPaused: () => appState.playbackPaused, - getSubtitlePosition: () => loadSubtitlePosition(), - getSubtitleStyle: () => { - const resolvedConfig = getResolvedConfig(); - return (0, overlay_1.resolveSubtitleStyleForRenderer)(resolvedConfig); - }, - saveSubtitlePosition: (position) => saveSubtitlePosition(position), - getMecabTokenizer: () => appState.mecabTokenizer, - getKeybindings: () => appState.keybindings, - getSessionBindings: () => appState.sessionBindings, - getConfiguredShortcuts: () => getConfiguredShortcuts(), - dispatchSessionAction: (request) => dispatchSessionAction(request), - getStatsToggleKey: () => getResolvedConfig().stats.toggleKey, - getMarkWatchedKey: () => getResolvedConfig().stats.markWatchedKey, - getControllerConfig: () => getResolvedConfig().controller, - saveControllerConfig: (update) => { - const currentRawConfig = configService.getRawConfig(); - configService.patchRawConfig({ - controller: (0, controller_config_update_js_1.applyControllerConfigUpdate)(currentRawConfig.controller, update), - }); - }, - saveControllerPreference: ({ preferredGamepadId, preferredGamepadLabel }) => { - configService.patchRawConfig({ - controller: { - preferredGamepadId, - preferredGamepadLabel, - }, - }); - }, - getSecondarySubMode: () => appState.secondarySubMode, - getMpvClient: () => appState.mpvClient, - getAnkiConnectStatus: () => appState.ankiIntegration !== null, - getRuntimeOptions: () => getRuntimeOptionsState(), - reportOverlayContentBounds: (payload) => { - overlayContentMeasurementStore.report(payload); - }, - getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(), - clearAnilistToken: () => anilistStateRuntime.clearTokenState(), - openAnilistSetup: () => openAnilistSetupWindow(), - getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(), - retryAnilistQueueNow: () => processNextAnilistRetryUpdate(), - runAnilistPostWatchUpdateOnManualMark: () => maybeRunAnilistPostWatchUpdate({ force: true }), - getCharacterDictionarySelection: () => characterDictionaryRuntime.getManualSelectionSnapshot(), - setCharacterDictionarySelection: async (mediaId) => (0, character_dictionary_selection_1.applyCharacterDictionarySelection)({ mediaId }, { - setManualSelection: (request) => characterDictionaryRuntime.setManualSelection(request), - resetAnilistMediaGuessState, - runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(), - warn: (message, error) => logger.warn(message, error), - }), - appendClipboardVideoToQueue: () => appendClipboardVideoToQueue(), - ...playlistBrowserMainDeps, - getImmersionTracker: () => appState.immersionTracker, - }, - ankiJimakuDeps: (0, dependencies_1.createAnkiJimakuIpcRuntimeServiceDeps)({ - patchAnkiConnectEnabled: (enabled) => { - configService.patchRawConfig({ ankiConnect: { enabled } }); - }, - getResolvedConfig: () => getResolvedConfig(), - getRuntimeOptionsManager: () => appState.runtimeOptionsManager, - getSubtitleTimingTracker: () => appState.subtitleTimingTracker, - getMpvClient: () => appState.mpvClient, - getAnkiIntegration: () => appState.ankiIntegration, - setAnkiIntegration: (integration) => { - appState.ankiIntegration = integration; - appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined); - appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAfterKnownWordUpdate); - }, - getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'), - showDesktopNotification: utils_2.showDesktopNotification, - createFieldGroupingCallback: () => createFieldGroupingCallback(), - broadcastRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(), - getFieldGroupingResolver: () => getFieldGroupingResolver(), - setFieldGroupingResolver: (resolver) => setFieldGroupingResolver(resolver), - parseMediaInfo: (mediaPath) => (0, utils_1.parseMediaInfo)(mediaRuntime.resolveMediaPathForJimaku(mediaPath)), - getCurrentMediaPath: () => appState.currentMediaPath, - jimakuFetchJson: (endpoint, query) => configDerivedRuntime.jimakuFetchJson(endpoint, query), - getJimakuMaxEntryResults: () => configDerivedRuntime.getJimakuMaxEntryResults(), - getJimakuLanguagePreference: () => configDerivedRuntime.getJimakuLanguagePreference(), - resolveJimakuApiKey: () => configDerivedRuntime.resolveJimakuApiKey(), - isRemoteMediaPath: (mediaPath) => (0, utils_1.isRemoteMediaPath)(mediaPath), - downloadToFile: (url, destPath, headers) => (0, utils_1.downloadToFile)(url, destPath, headers), - }), - registerIpcRuntimeServices: ipc_runtime_1.registerIpcRuntimeServices, - }, -}); -const { handleCliCommand, handleInitialArgs } = (0, composers_1.composeCliStartupHandlers)({ - cliCommandContextMainDeps: { - appState, - setLogLevel: (level) => (0, logger_1.setLogLevel)(level, 'cli'), - texthookerService, - getResolvedConfig: () => getResolvedConfig(), - defaultWebsocketPort: config_2.DEFAULT_CONFIG.websocket.port, - defaultAnnotationWebsocketPort: config_2.DEFAULT_CONFIG.annotationWebsocket.port, - hasMpvWebsocketPlugin: () => (0, services_1.hasMpvWebsocketPlugin)(), - openExternal: (url) => electron_1.shell.openExternal(url), - logBrowserOpenError: (url, error) => logger.error(`Failed to open browser for texthooker URL: ${url}`, error), - showMpvOsd: (text) => showMpvOsd(text), - initializeOverlayRuntime: () => initializeOverlayRuntime(), - toggleVisibleOverlay: () => toggleVisibleOverlay(), - togglePrimarySubtitleBar: () => togglePrimarySubtitleBar(), - openFirstRunSetupWindow: (force) => openFirstRunSetupWindow(force), - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - copyCurrentSubtitle: () => copyCurrentSubtitle(), - startPendingMultiCopy: (timeoutMs) => startPendingMultiCopy(timeoutMs), - mineSentenceCard: () => mineSentenceCard(), - startPendingMineSentenceMultiple: (timeoutMs) => startPendingMineSentenceMultiple(timeoutMs), - updateLastCardFromClipboard: () => updateLastCardFromClipboard(), - refreshKnownWordCache: () => refreshKnownWordCache(), - triggerFieldGrouping: () => triggerFieldGrouping(), - triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), - markLastCardAsAudioCard: () => markLastCardAsAudioCard(), - getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(), - clearAnilistToken: () => anilistStateRuntime.clearTokenState(), - openAnilistSetupWindow: () => openAnilistSetupWindow(), - openJellyfinSetupWindow: () => openJellyfinSetupWindow(), - getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(), - processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(), - generateCharacterDictionary: async (targetPath) => { - const disabledReason = yomitanProfilePolicy.getCharacterDictionaryDisabledReason(); - if (disabledReason) { - throw new Error(disabledReason); - } - return await characterDictionaryRuntime.generateForCurrentMedia(targetPath); - }, - getCharacterDictionarySelection: async (targetPath) => characterDictionaryRuntime.getManualSelectionSnapshot(targetPath), - setCharacterDictionarySelection: async (request) => (0, character_dictionary_selection_1.applyCharacterDictionarySelection)(request, { - setManualSelection: (selectionRequest) => characterDictionaryRuntime.setManualSelection(selectionRequest), - resetAnilistMediaGuessState, - runSyncNow: () => characterDictionaryAutoSyncRuntime.runSyncNow(), - warn: (message, error) => logger.warn(message, error), - }), - runJellyfinCommand: (argsFromCommand) => runJellyfinCommand(argsFromCommand), - runStatsCommand: (argsFromCommand, source) => runStatsCliCommand(argsFromCommand, source), - runUpdateCommand: async (argsFromCommand, source) => { - await (0, update_cli_command_1.runUpdateCliCommand)(argsFromCommand, source, { - checkForUpdates: (request) => getUpdateService().checkForUpdates(request), - writeResponse: (responsePath, payload) => (0, update_cli_command_1.writeUpdateCliCommandResponse)(responsePath, payload), - logWarn: (message, error) => logger.warn(message, error), - }); - }, - runYoutubePlaybackFlow: (request) => youtubePlaybackRuntime.runYoutubePlaybackFlow(request), - openYomitanSettings: () => openYomitanSettings(), - openConfigSettingsWindow: () => openConfigSettingsWindow(), - cycleSecondarySubMode: () => handleCycleSecondarySubMode(), - openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), - printHelp: () => (0, help_1.printHelp)(DEFAULT_TEXTHOOKER_PORT), - stopApp: () => requestAppQuit(), - hasMainWindow: () => Boolean(overlayManager.getMainWindow()), - dispatchSessionAction: (request) => dispatchSessionAction(request), - getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, - schedule: (fn, delayMs) => setTimeout(fn, delayMs), - logInfo: (message) => logger.info(message), - logDebug: (message) => logger.debug(message), - logWarn: (message) => logger.warn(message), - logError: (message, err) => logger.error(message, err), - }, - cliCommandRuntimeHandlerMainDeps: { - handleTexthookerOnlyModeTransitionMainDeps: { - isTexthookerOnlyMode: () => appState.texthookerOnlyMode, - ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(), - setTexthookerOnlyMode: (enabled) => { - appState.texthookerOnlyMode = enabled; - }, - commandNeedsOverlayStartupPrereqs: (inputArgs) => (0, args_1.commandNeedsOverlayStartupPrereqs)(inputArgs), - startBackgroundWarmups: () => startBackgroundWarmups(), - logInfo: (message) => logger.info(message), - }, - handleCliCommandRuntimeServiceWithContext: (args, source, cliContext) => (0, cli_runtime_1.handleCliCommandRuntimeServiceWithContext)(args, source, cliContext), - }, - initialArgsRuntimeHandlerMainDeps: { - getInitialArgs: () => appState.initialArgs, - isBackgroundMode: () => appState.backgroundMode, - shouldEnsureTrayOnStartup: () => (0, startup_tray_policy_1.shouldEnsureTrayOnStartupForInitialArgs)(process.platform, appState.initialArgs), - shouldRunHeadlessInitialCommand: (args) => (0, args_1.isHeadlessInitialCommand)(args), - ensureTray: () => ensureTray(), - isTexthookerOnlyMode: () => appState.texthookerOnlyMode, - hasImmersionTracker: () => Boolean(appState.immersionTracker), - getMpvClient: () => appState.mpvClient, - commandNeedsOverlayStartupPrereqs: (args) => (0, args_1.commandNeedsOverlayStartupPrereqs)(args), - commandNeedsOverlayRuntime: (args) => (0, args_1.commandNeedsOverlayRuntime)(args), - ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(), - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - initializeOverlayRuntime: () => initializeOverlayRuntime(), - logInfo: (message) => logger.info(message), - }, -}); -const { runAndApplyStartupState } = (0, composers_1.composeHeadlessStartupHandlers)({ - startupRuntimeHandlersDeps: { - appLifecycleRuntimeRunnerMainDeps: { - app: appLifecycleApp, - platform: process.platform, - shouldStartApp: (nextArgs) => (0, args_1.shouldStartApp)(nextArgs), - parseArgs: (argv) => (0, args_1.parseArgs)(argv), - handleCliCommand: (nextArgs, source) => handleCliCommand(nextArgs, source), - printHelp: () => (0, help_1.printHelp)(DEFAULT_TEXTHOOKER_PORT), - logNoRunningInstance: () => appLogger.logNoRunningInstance(), - onReady: appReadyRuntimeRunner, - onWillQuitCleanup: () => onWillQuitCleanupHandler(), - shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(), - restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(), - shouldQuitOnWindowAllClosed: () => (0, startup_tray_policy_1.shouldQuitOnWindowAllClosedForTrayState)({ - backgroundMode: appState.backgroundMode, - hasTray: Boolean(appTray), - }), - }, - createAppLifecycleRuntimeRunner: (params) => (0, startup_lifecycle_1.createAppLifecycleRuntimeRunner)(params), - buildStartupBootstrapMainDeps: (startAppLifecycle) => ({ - argv: process.argv, - parseArgs: (argv) => (0, args_1.parseArgs)(argv), - setLogLevel: (level, source) => { - (0, logger_1.setLogLevel)(level, source); - }, - forceX11Backend: (args) => { - (0, utils_2.forceX11Backend)(args); - }, - enforceUnsupportedWaylandMode: (args) => { - (0, utils_2.enforceUnsupportedWaylandMode)(args); - }, - shouldStartApp: (args) => (0, args_1.shouldStartApp)(args), - getDefaultSocketPath: () => getResolvedConfig().mpv.socketPath || getDefaultSocketPath(), - defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT, - configDir: CONFIG_DIR, - defaultConfig: config_2.DEFAULT_CONFIG, - generateConfigTemplate: (config) => (0, config_2.generateConfigTemplate)(config), - generateDefaultConfigFile: (args, options) => (0, utils_2.generateDefaultConfigFile)(args, options), - setExitCode: (code) => { - process.exitCode = code; - }, - quitApp: () => requestAppQuit(), - logGenerateConfigError: (message) => logger.error(message), - startAppLifecycle, - }), - createStartupBootstrapRuntimeDeps: (deps) => (0, startup_2.createStartupBootstrapRuntimeDeps)(deps), - runStartupBootstrapRuntime: services_1.runStartupBootstrapRuntime, - applyStartupState: (startupState) => (0, state_1.applyStartupState)(appState, startupState), - }, -}); -runAndApplyStartupState(); -void electron_1.app.whenReady().then(() => { - if (!(0, startup_mode_flags_1.shouldStartAutomaticUpdateChecks)(appState.initialArgs)) { - return; - } - getUpdateService().startAutomaticChecks(); -}); -const startupModeFlags = (0, startup_mode_flags_1.getStartupModeFlags)(appState.initialArgs); -const shouldUseMinimalStartup = startupModeFlags.shouldUseMinimalStartup; -const shouldSkipHeavyStartup = startupModeFlags.shouldSkipHeavyStartup; -if (!appState.initialArgs || (!shouldUseMinimalStartup && !shouldSkipHeavyStartup)) { - if ((0, anilist_1.isAnilistTrackingEnabled)(getResolvedConfig())) { - void refreshAnilistClientSecretStateIfEnabled({ - force: true, - allowSetupPrompt: false, - }).catch((error) => { - logger.error('Failed to refresh AniList client secret state during startup', error); - }); - anilistStateRuntime.refreshRetryQueueState(); - } - void initializeDiscordPresenceService().catch((error) => { - logger.error('Failed to initialize Discord presence service during startup', error); - }); -} -const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } = (0, overlay_window_runtime_handlers_1.createOverlayWindowRuntimeHandlers)({ - createOverlayWindowDeps: { - createOverlayWindowCore: (kind, options) => (0, services_1.createOverlayWindow)(kind, options), - isDev, - ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), - onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(), - setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled), - isOverlayVisible: (windowKind) => windowKind === 'visible' ? overlayManager.getVisibleOverlayVisible() : false, - getYomitanSession: () => appState.yomitanSession, - tryHandleOverlayShortcutLocalFallback: (input) => overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input), - forwardTabToMpv: () => (0, services_1.sendMpvCommandRuntime)(appState.mpvClient, ['keypress', 'TAB']), - onVisibleWindowBlurred: () => scheduleVisibleOverlayBlurRefresh(), - onWindowContentReady: () => { - overlayVisibilityRuntime.updateVisibleOverlayVisibility(); - if (appState.currentSubText.trim()) { - subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText); - } - }, - onWindowClosed: (windowKind) => { - if (windowKind === 'visible') { - cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(); - overlayManager.setMainWindow(null); - } - else { - overlayManager.setModalWindow(null); - } - }, - }, - setMainWindow: (window) => overlayManager.setMainWindow(window), - setModalWindow: (window) => overlayManager.setModalWindow(window), -}); -function refreshTrayMenuIfPresent() { - if (appTray) { - ensureTrayHandler(); - } -} -function getJellyfinTrayDiscoveryDeps() { - return { - getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(), - getRemoteSession: () => appState.jellyfinRemoteSession, - clearStoredSession: () => jellyfinTokenStore.clearSession(), - stopRemoteSession: () => stopJellyfinRemoteSession(), - startRemoteSession: (options) => startJellyfinRemoteSession(options), - refreshTrayMenu: () => refreshTrayMenuIfPresent(), - logger, - showMpvOsd: (message) => showMpvOsd(message), - }; -} -const { ensureTray: ensureTrayHandler, destroyTray: destroyTrayHandler } = (0, overlay_1.createTrayRuntimeHandlers)({ - resolveTrayIconPathDeps: { - resolveTrayIconPathRuntime: overlay_1.resolveTrayIconPathRuntime, - platform: process.platform, - resourcesPath: process.resourcesPath, - appPath: electron_1.app.getAppPath(), - dirname: __dirname, - joinPath: (...parts) => path.join(...parts), - fileExists: (candidate) => fs.existsSync(candidate), - }, - buildTrayMenuTemplateDeps: { - buildTrayMenuTemplateRuntime: overlay_1.buildTrayMenuTemplateRuntime, - initializeOverlayRuntime: () => initializeOverlayRuntime(), - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - openSessionHelpModal: () => openSessionHelpOverlay(), - openTexthookerInBrowser: () => handleCliCommand((0, args_1.parseArgs)(['--texthooker', '--open-browser'])), - showTexthookerPage: () => getResolvedConfig().texthooker.launchAtStartup !== false, - showFirstRunSetup: () => !firstRunSetupService.isSetupCompleted(), - openFirstRunSetupWindow: () => openFirstRunSetupWindow(), - showWindowsMpvLauncherSetup: () => process.platform === 'win32', - openYomitanSettings: () => openYomitanSettings(), - openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), - openConfigSettingsWindow: () => openConfigSettingsWindow(), - openJellyfinSetupWindow: () => openJellyfinSetupWindow(), - isJellyfinConfigured: () => (0, jellyfin_tray_discovery_1.isJellyfinConfiguredForTray)(getJellyfinTrayDiscoveryDeps()), - isJellyfinDiscoveryActive: () => Boolean(appState.jellyfinRemoteSession), - toggleJellyfinDiscovery: () => (0, jellyfin_tray_discovery_1.toggleJellyfinDiscoveryFromTray)(getJellyfinTrayDiscoveryDeps()), - openAnilistSetupWindow: () => openAnilistSetupWindow(), - checkForUpdates: () => { - void getUpdateService().checkForUpdates({ source: 'manual' }); - }, - quitApp: () => requestAppQuit(), - }, - ensureTrayDeps: { - getTray: () => appTray, - setTray: (tray) => { - appTray = tray; - }, - createImageFromPath: (iconPath) => electron_1.nativeImage.createFromPath(iconPath), - createEmptyImage: () => electron_1.nativeImage.createEmpty(), - createTray: (icon) => new electron_1.Tray(icon), - trayTooltip: TRAY_TOOLTIP, - platform: process.platform, - logWarn: (message) => logger.warn(message), - initializeOverlayRuntime: () => initializeOverlayRuntime(), - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), - }, - destroyTrayDeps: { - getTray: () => appTray, - setTray: (tray) => { - appTray = tray; - }, - }, - buildMenuFromTemplate: (template) => electron_1.Menu.buildFromTemplate(template), -}); -const yomitanProfilePolicy = (0, yomitan_profile_policy_1.createYomitanProfilePolicy)({ - externalProfilePath: getResolvedConfig().yomitan.externalProfilePath, - logInfo: (message) => logger.info(message), -}); -const configuredExternalYomitanProfilePath = yomitanProfilePolicy.externalProfilePath; -const yomitanExtensionRuntime = (0, overlay_1.createYomitanExtensionRuntime)({ - loadYomitanExtensionCore: services_1.loadYomitanExtension, - userDataPath: USER_DATA_PATH, - externalProfilePath: configuredExternalYomitanProfilePath, - getYomitanParserWindow: () => appState.yomitanParserWindow, - setYomitanParserWindow: (window) => { - appState.yomitanParserWindow = window; - }, - setYomitanParserReadyPromise: (promise) => { - appState.yomitanParserReadyPromise = promise; - }, - setYomitanParserInitPromise: (promise) => { - appState.yomitanParserInitPromise = promise; - }, - setYomitanExtension: (extension) => { - appState.yomitanExt = extension; - }, - setYomitanSession: (nextSession) => { - appState.yomitanSession = nextSession; - }, - getYomitanExtension: () => appState.yomitanExt, - getLoadInFlight: () => yomitanLoadInFlight, - setLoadInFlight: (promise) => { - yomitanLoadInFlight = promise; - }, -}); -const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } = runtimeRegistry.overlay.createOverlayRuntimeBootstrapHandlers({ - initializeOverlayRuntimeMainDeps: { - appState, - overlayManager: { - getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), - }, - overlayVisibilityRuntime: { - updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(), - }, - refreshCurrentSubtitle: () => { - subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText); - }, - overlayShortcutsRuntime: { - syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(), - }, - createMainWindow: () => { - if (appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)) { - return; - } - createMainWindow(); - }, - registerGlobalShortcuts: () => { - if (appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)) { - return; - } - registerGlobalShortcuts(); - }, - createWindowTracker: (override, targetMpvSocketPath) => { - if (appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)) { - return null; - } - return (0, window_trackers_1.createWindowTracker)(override, targetMpvSocketPath); - }, - updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry), - bindOverlayOwner: () => { - const mainWindow = overlayManager.getMainWindow(); - if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) - return; - const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); - const targetWindowHwnd = resolveWindowsOverlayBindTargetHandle(appState.mpvSocketPath); - if (targetWindowHwnd !== null && - (0, windows_helper_1.bindWindowsOverlayAboveMpv)(overlayHwnd, targetWindowHwnd)) { - return; - } - const tracker = appState.windowTracker; - const mpvResult = tracker - ? (() => { - try { - const win32 = require('./window-trackers/win32'); - const poll = win32.findMpvWindows(); - const focused = poll.matches.find((m) => m.isForeground); - return focused ?? [...poll.matches].sort((a, b) => b.area - a.area)[0] ?? null; - } - catch { - return null; - } - })() - : null; - if (!mpvResult) - return; - if (!(0, windows_helper_1.setWindowsOverlayOwner)(overlayHwnd, mpvResult.hwnd)) { - logger.warn('Failed to set overlay owner via koffi'); - } - }, - releaseOverlayOwner: () => { - const mainWindow = overlayManager.getMainWindow(); - if (process.platform !== 'win32' || !mainWindow || mainWindow.isDestroyed()) - return; - const overlayHwnd = getWindowsNativeWindowHandleNumber(mainWindow); - if (!(0, windows_helper_1.clearWindowsOverlayOwner)(overlayHwnd)) { - logger.warn('Failed to clear overlay owner via koffi'); - } - }, - getOverlayWindows: () => getOverlayWindows(), - getResolvedConfig: () => getResolvedConfig(), - showDesktopNotification: utils_2.showDesktopNotification, - createFieldGroupingCallback: () => createFieldGroupingCallback(), - getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'), - shouldStartAnkiIntegration: () => !(appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)), - }, - initializeOverlayRuntimeBootstrapDeps: { - isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - initializeOverlayRuntimeCore: services_1.initializeOverlayRuntime, - setOverlayRuntimeInitialized: (initialized) => { - appState.overlayRuntimeInitialized = initialized; - }, - startBackgroundWarmups: () => { - if (appState.initialArgs && (0, args_1.isHeadlessInitialCommand)(appState.initialArgs)) { - return; - } - startBackgroundWarmups(); - }, - }, -}); -const { openYomitanSettings: openYomitanSettingsHandler } = (0, overlay_1.createYomitanSettingsRuntime)({ - ensureYomitanExtensionLoaded: () => ensureYomitanExtensionLoaded(), - getYomitanExtension: () => appState.yomitanExt, - getYomitanExtensionLoadInFlight: () => yomitanLoadInFlight, - getYomitanSession: () => appState.yomitanSession, - openYomitanSettingsWindow: ({ yomitanExt, getExistingWindow, setWindow, yomitanSession }) => { - (0, services_1.openYomitanSettingsWindow)({ - yomitanExt: yomitanExt, - getExistingWindow: () => getExistingWindow(), - setWindow: (window) => setWindow(window), - yomitanSession: yomitanSession ?? appState.yomitanSession, - onWindowClosed: () => { - if (appState.yomitanParserWindow) { - (0, services_1.clearYomitanParserCachesForWindow)(appState.yomitanParserWindow); - } - }, - }); - }, - getExistingWindow: () => appState.yomitanSettingsWindow, - setWindow: (window) => { - appState.yomitanSettingsWindow = window; - }, - logWarn: (message) => logger.warn(message), - logError: (message, error) => logger.error(message, error), -}); -async function updateLastCardFromClipboard() { - await updateLastCardFromClipboardHandler(); -} -async function refreshKnownWordCache() { - await refreshKnownWordCacheHandler(); -} -async function triggerFieldGrouping() { - await triggerFieldGroupingHandler(); -} -async function markLastCardAsAudioCard() { - await markLastCardAsAudioCardHandler(); -} -async function mineSentenceCard() { - await mineSentenceCardHandler(); -} -function handleMineSentenceDigit(count) { - handleMineSentenceDigitHandler(count); -} -function ensureOverlayWindowsReadyForVisibilityActions() { - if (!appState.overlayRuntimeInitialized) { - initializeOverlayRuntime(); - return; - } - const mainWindow = overlayManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - createMainWindow(); - } -} -function setVisibleOverlayVisible(visible) { - ensureOverlayWindowsReadyForVisibilityActions(); - if (!visible) { - cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(); - } - if (visible) { - void ensureOverlayMpvSubtitlesHidden(); - } - setVisibleOverlayVisibleHandler(visible); - syncOverlayMpvSubtitleSuppression(); -} -function toggleVisibleOverlay() { - ensureOverlayWindowsReadyForVisibilityActions(); - if (overlayManager.getVisibleOverlayVisible()) { - cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(); - } - else { - void ensureOverlayMpvSubtitlesHidden(); - } - toggleVisibleOverlayHandler(); - syncOverlayMpvSubtitleSuppression(); -} -function setOverlayVisible(visible) { - if (!visible) { - cancelPendingLinuxMpvFullscreenOverlayRefreshBurst(); - } - if (visible) { - void ensureOverlayMpvSubtitlesHidden(); - } - setOverlayVisibleHandler(visible); - syncOverlayMpvSubtitleSuppression(); -} -function handleOverlayModalClosed(modal) { - handleOverlayModalClosedHandler(modal); -} -function appendClipboardVideoToQueue() { - return appendClipboardVideoToQueueHandler(); -} -registerIpcRuntimeHandlers(); -//# sourceMappingURL=main.js.map diff --git a/src/cli/args.test.ts b/src/cli/args.test.ts index 07575a0c..243efa99 100644 --- a/src/cli/args.test.ts +++ b/src/cli/args.test.ts @@ -7,7 +7,7 @@ import { isHeadlessInitialCommand, isStandaloneTexthookerCommand, parseArgs, - shouldRunSettingsOnlyStartup, + shouldRunYomitanOnlyStartup, shouldStartApp, } from './args'; @@ -66,7 +66,7 @@ test('parseArgs captures update command and internal launcher paths', () => { assert.equal(hasExplicitCommand(args), true); assert.equal(shouldStartApp(args), true); assert.equal(commandNeedsOverlayRuntime(args), false); - assert.equal(shouldRunSettingsOnlyStartup(args), false); + assert.equal(shouldRunYomitanOnlyStartup(args), false); }); test('parseArgs captures launch-mpv targets and keeps it out of app startup', () => { @@ -208,35 +208,33 @@ test('hasExplicitCommand and shouldStartApp preserve command intent', () => { assert.equal(shouldStartApp(update), true); assert.equal(isHeadlessInitialCommand(update), true); + const yomitan = parseArgs(['--yomitan']); + assert.equal(yomitan.yomitan, true); + assert.equal(hasExplicitCommand(yomitan), true); + assert.equal(shouldStartApp(yomitan), true); + assert.equal(shouldRunYomitanOnlyStartup(yomitan), true); + const settings = parseArgs(['--settings']); assert.equal(settings.settings, true); assert.equal(hasExplicitCommand(settings), true); assert.equal(shouldStartApp(settings), true); - assert.equal(shouldRunSettingsOnlyStartup(settings), true); + assert.equal(shouldRunYomitanOnlyStartup(settings), false); + assert.equal(commandNeedsOverlayRuntime(settings), false); + assert.equal(commandNeedsOverlayStartupPrereqs(settings), false); - const configSettings = parseArgs(['--config']); - assert.equal(configSettings.configSettings, true); - assert.equal(hasExplicitCommand(configSettings), true); - assert.equal(shouldStartApp(configSettings), true); - assert.equal(shouldRunSettingsOnlyStartup(configSettings), false); - assert.equal(commandNeedsOverlayRuntime(configSettings), false); - assert.equal(commandNeedsOverlayStartupPrereqs(configSettings), false); + const yomitanWithOverlay = parseArgs(['--yomitan', '--toggle-visible-overlay']); + assert.equal(yomitanWithOverlay.yomitan, true); + assert.equal(yomitanWithOverlay.toggleVisibleOverlay, true); + assert.equal(shouldRunYomitanOnlyStartup(yomitanWithOverlay), false); - const settingsWithOverlay = parseArgs(['--settings', '--toggle-visible-overlay']); - assert.equal(settingsWithOverlay.settings, true); - assert.equal(settingsWithOverlay.toggleVisibleOverlay, true); - assert.equal(shouldRunSettingsOnlyStartup(settingsWithOverlay), false); - - const yomitanAlias = parseArgs(['--yomitan']); - assert.equal(yomitanAlias.settings, true); - assert.equal(hasExplicitCommand(yomitanAlias), true); - assert.equal(shouldStartApp(yomitanAlias), true); + const settingsDoesNotEnableYomitan = parseArgs(['--settings']); + assert.equal(settingsDoesNotEnableYomitan.yomitan, false); const help = parseArgs(['--help']); assert.equal(help.help, true); assert.equal(hasExplicitCommand(help), true); assert.equal(shouldStartApp(help), false); - assert.equal(shouldRunSettingsOnlyStartup(help), false); + assert.equal(shouldRunYomitanOnlyStartup(help), false); const appPing = parseArgs(['--app-ping']); assert.equal(appPing.appPing, true); diff --git a/src/cli/args.ts b/src/cli/args.ts index 0a1977c3..ec6b6a88 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -10,8 +10,8 @@ export interface CliArgs { toggle: boolean; toggleVisibleOverlay: boolean; togglePrimarySubtitleBar: boolean; + yomitan: boolean; settings: boolean; - configSettings: boolean; setup: boolean; show: boolean; hide: boolean; @@ -117,8 +117,8 @@ export function parseArgs(argv: string[]): CliArgs { toggle: false, toggleVisibleOverlay: false, togglePrimarySubtitleBar: false, + yomitan: false, settings: false, - configSettings: false, setup: false, show: false, hide: false, @@ -239,8 +239,8 @@ export function parseArgs(argv: string[]): CliArgs { else if (arg === '--toggle') args.toggle = true; else if (arg === '--toggle-visible-overlay') args.toggleVisibleOverlay = true; else if (arg === '--toggle-primary-subtitle-bar') args.togglePrimarySubtitleBar = true; - else if (arg === '--settings' || arg === '--yomitan') args.settings = true; - else if (arg === '--config') args.configSettings = true; + else if (arg === '--yomitan') args.yomitan = true; + else if (arg === '--settings') args.settings = true; else if (arg === '--setup') args.setup = true; else if (arg === '--show') args.show = true; else if (arg === '--hide') args.hide = true; @@ -494,8 +494,8 @@ export function hasExplicitCommand(args: CliArgs): boolean { args.toggle || args.toggleVisibleOverlay || args.togglePrimarySubtitleBar || + args.yomitan || args.settings || - args.configSettings || args.setup || args.show || args.hide || @@ -569,8 +569,8 @@ export function isStandaloneTexthookerCommand(args: CliArgs): boolean { !args.toggle && !args.toggleVisibleOverlay && !args.togglePrimarySubtitleBar && + !args.yomitan && !args.settings && - !args.configSettings && !args.setup && !args.show && !args.hide && @@ -639,8 +639,8 @@ export function shouldStartApp(args: CliArgs): boolean { args.toggle || args.toggleVisibleOverlay || args.togglePrimarySubtitleBar || + args.yomitan || args.settings || - args.configSettings || args.setup || args.copySubtitle || args.copySubtitleMultiple || @@ -687,16 +687,16 @@ export function shouldStartApp(args: CliArgs): boolean { return false; } -export function shouldRunSettingsOnlyStartup(args: CliArgs): boolean { +export function shouldRunYomitanOnlyStartup(args: CliArgs): boolean { return ( - args.settings && + args.yomitan && !args.background && !args.start && !args.stop && !args.toggle && !args.toggleVisibleOverlay && !args.togglePrimarySubtitleBar && - !args.configSettings && + !args.settings && !args.show && !args.hide && !args.setup && diff --git a/src/cli/help.test.ts b/src/cli/help.test.ts index b169c714..7e46f330 100644 --- a/src/cli/help.test.ts +++ b/src/cli/help.test.ts @@ -22,7 +22,8 @@ test('printHelp includes configured texthooker port', () => { assert.match(output, /--open-browser\s+Open texthooker in your default browser/); assert.doesNotMatch(output, /--refresh-known-words/); assert.match(output, /--setup\s+Open first-run setup window/); - assert.match(output, /--config\s+Open configuration window/); + assert.match(output, /--settings\s+Open SubMiner settings window/); + assert.match(output, /--yomitan\s+Open Yomitan settings window/); assert.match(output, /--mark-watched\s+Mark current video watched and advance playlist/); assert.match(output, /--anilist-status/); assert.match(output, /--anilist-retry-queue/); diff --git a/src/cli/help.ts b/src/cli/help.ts index 7eca51ff..e9588286 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -24,8 +24,8 @@ ${B}Overlay${R} --toggle-primary-subtitle-bar Toggle primary subtitle bar --show-visible-overlay Show subtitle overlay --hide-visible-overlay Hide subtitle overlay - --settings Open Yomitan settings window - --config Open configuration window + --yomitan Open Yomitan settings window + --settings Open SubMiner settings window --setup Open first-run setup window --auto-start-overlay Auto-hide mpv subs, show overlay on connect diff --git a/src/config/settings/registry.test.ts b/src/config/settings/registry.test.ts index 10a92b92..2b9e539b 100644 --- a/src/config/settings/registry.test.ts +++ b/src/config/settings/registry.test.ts @@ -19,17 +19,77 @@ test('settings registry splits viewing into appearance and behavior categories', assert.equal(field('subtitlePosition.yPercent').label, 'Subtitle Position'); assert.equal(field('subtitleStyle.frequencyDictionary.mode').label, 'Frequency Mode'); assert.equal(field('auto_start_overlay').category, 'behavior'); - assert.equal(field('auto_start_overlay').section, 'Visible Overlay Auto-Start'); + assert.equal(field('auto_start_overlay').section, 'Playback Behavior'); assert.equal(field('youtube.primarySubLanguages').category, 'behavior'); assert.equal(field('youtube.primarySubLanguages').section, 'YouTube Playback Settings'); assert.equal(field('mpv.launchMode').category, 'behavior'); - assert.equal(field('mpv.launchMode').section, 'MPV Launcher'); + assert.equal(field('mpv.launchMode').section, 'mpv Playback'); assert.ok( fields.findIndex((candidate) => candidate.configPath === 'subtitleStyle.primaryDefaultMode') < fields.findIndex((candidate) => candidate.configPath === 'secondarySub.defaultMode'), ); }); +test('settings registry groups playback startup controls under playback behavior', () => { + for (const path of [ + 'subtitleStyle.autoPauseVideoOnHover', + 'subtitleStyle.autoPauseVideoOnYomitanPopup', + 'subtitleSidebar.pauseVideoOnHover', + 'mpv.autoStartSubMiner', + 'auto_start_overlay', + 'mpv.pauseUntilOverlayReady', + ]) { + assert.equal(field(path).category, 'behavior', path); + assert.equal(field(path).section, 'Playback Behavior', path); + } +}); + +test('settings registry moves AniSkip button key into input shortcuts and hot reload', () => { + assert.equal(field('mpv.aniskipButtonKey').category, 'input'); + assert.equal(field('mpv.aniskipButtonKey').section, 'Overlay Shortcuts'); + assert.equal(field('mpv.aniskipButtonKey').subsection, 'Playback'); + assert.equal(field('mpv.aniskipButtonKey').control, 'mpv-key'); + assert.equal(field('mpv.aniskipButtonKey').restartBehavior, 'hot-reload'); +}); + +test('settings registry hides removed modal-only fields', () => { + for (const path of [ + 'shortcuts.multiCopyTimeoutMs', + 'anilist.characterDictionary.profileScope', + 'jellyfin.directPlayContainers', + 'jellyfin.remoteControlDeviceName', + ]) { + assert.equal( + fields.some((candidate) => candidate.configPath === path), + false, + path, + ); + } +}); + +test('settings registry orders websocket server immediately after annotation websocket', () => { + const integrationSections = [ + ...new Set( + fields + .filter((candidate) => candidate.category === 'integrations') + .map((candidate) => candidate.section), + ), + ]; + const annotationIndex = integrationSections.indexOf('Annotation WebSocket'); + assert.equal(integrationSections[annotationIndex + 1], 'WebSocket server'); +}); + +test('settings registry places immersion tracking after other tracking and app sections', () => { + const trackingSections = [ + ...new Set( + fields + .filter((candidate) => candidate.category === 'tracking-app') + .map((candidate) => candidate.section), + ), + ]; + assert.equal(trackingSections.at(-1), 'Immersion tracking'); +}); + test('settings registry groups annotation display fields by config group', () => { assert.equal(field('ankiConnect.knownWords.highlightEnabled').section, 'Annotation Display'); assert.equal(field('ankiConnect.knownWords.highlightEnabled').subsection, 'Known Words'); @@ -190,6 +250,7 @@ test('settings registry hides app-managed and inactive config surfaces', () => { test('settings registry marks safe live config paths as hot-reloadable', () => { for (const path of [ + 'mpv.aniskipButtonKey', 'stats.toggleKey', 'stats.markWatchedKey', 'logging.level', diff --git a/src/config/settings/registry.ts b/src/config/settings/registry.ts index 8a21aa35..5b23ec6d 100644 --- a/src/config/settings/registry.ts +++ b/src/config/settings/registry.ts @@ -65,13 +65,17 @@ export const LEGACY_HIDDEN_CONFIG_PATHS = [ 'youtubeSubgen.primarySubLanguages', 'anilist.characterDictionary.refreshTtlHours', 'anilist.characterDictionary.evictionPolicy', + 'anilist.characterDictionary.profileScope', 'jellyfin.accessToken', 'jellyfin.userId', 'jellyfin.clientName', 'jellyfin.clientVersion', 'jellyfin.defaultLibraryId', 'jellyfin.deviceId', + 'jellyfin.directPlayContainers', + 'jellyfin.remoteControlDeviceName', 'controller.buttonIndices', + 'shortcuts.multiCopyTimeoutMs', 'subtitleSidebar.toggleKey', 'jellyfin.recentServers', ] as const; @@ -123,12 +127,11 @@ const SECTION_ORDER = new Map( 'Primary Subtitle Appearance', 'Secondary Subtitle Appearance', 'Subtitle Sidebar Appearance', - 'Playback Pause Behavior', + 'Playback Behavior', 'Subtitle Behavior', 'Subtitle Sidebar Behavior', - 'Visible Overlay Auto-Start', 'YouTube Playback Settings', - 'MPV Launcher', + 'mpv Playback', 'Note Fields', 'Media Capture', 'Kiku/Lapis Features', @@ -140,7 +143,19 @@ const SECTION_ORDER = new Map( 'MPV Keybindings', 'Overlay Shortcuts', 'Controller', + 'Annotation WebSocket', + 'WebSocket server', + 'AniList', 'Character Dictionary', + 'Discord Rich Presence', + 'Jellyfin', + 'Texthooker', + 'Yomitan', + 'Stats dashboard', + 'Startup warmups', + 'Logging', + 'Updates', + 'Immersion tracking', ].map((section, index) => [section, index]), ); @@ -169,9 +184,9 @@ const PATH_ORDER = new Map( 'mpv.backend', 'mpv.subminerBinaryPath', 'mpv.aniskipEnabled', - 'mpv.aniskipButtonKey', 'mpv.launchMode', 'mpv.executablePath', + 'mpv.aniskipButtonKey', ].map((path, index) => [path, index]), ); @@ -186,7 +201,6 @@ const SUBSECTION_ORDER = new Map( 'Toggle & Visibility', 'Open Panels', 'Playback', - 'Timing', 'Default Fold State', ].map((subsection, index) => [subsection, index]), ); @@ -215,6 +229,7 @@ const LABEL_OVERRIDES: Record = { 'mpv.pauseUntilOverlayReady': 'Pause Until Overlay Ready', 'mpv.aniskipEnabled': 'Enable AniSkip', 'mpv.aniskipButtonKey': 'AniSkip Button Key', + 'discordPresence.updateIntervalMs': 'Update Interval Seconds', }; const DESCRIPTION_OVERRIDES: Record = { @@ -232,6 +247,8 @@ const DESCRIPTION_OVERRIDES: Record = { 'CSS declarations applied to secondary subtitles. Includes color, background-color, and all font properties.', 'subtitleSidebar.css': 'CSS declarations applied to the subtitle sidebar. Includes color, background-color, all font properties, and sidebar CSS variables.', + 'discordPresence.updateIntervalMs': + 'Minimum interval between presence payload updates, in seconds.', }; function isRecord(value: unknown): value is Record { @@ -295,7 +312,7 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s path === 'subtitleStyle.autoPauseVideoOnYomitanPopup' || path === 'subtitleSidebar.pauseVideoOnHover' ) { - return { category: 'behavior', section: 'Playback Pause Behavior' }; + return { category: 'behavior', section: 'Playback Behavior' }; } if (path === 'subtitleStyle.preserveLineBreaks') { return { category: 'behavior', section: 'Subtitle Behavior' }; @@ -373,8 +390,15 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s if (path.startsWith('ankiConnect.')) { return { category: 'mining-anki', section: 'AnkiConnect' }; } - if (path === 'auto_start_overlay') { - return { category: 'behavior', section: topSection(path) }; + if ( + path === 'auto_start_overlay' || + path === 'mpv.autoStartSubMiner' || + path === 'mpv.pauseUntilOverlayReady' + ) { + return { category: 'behavior', section: 'Playback Behavior' }; + } + if (path === 'mpv.aniskipButtonKey') { + return { category: 'input', section: 'Overlay Shortcuts' }; } if (path.startsWith('mpv.') || path.startsWith('youtube.')) { return { category: 'behavior', section: topSection(path) }; @@ -437,7 +461,7 @@ function topSection(path: string): string { jimaku: 'Jimaku', jellyfin: 'Jellyfin', logging: 'Logging', - mpv: 'MPV Launcher', + mpv: 'mpv Playback', stats: 'Stats dashboard', startupWarmups: 'Startup warmups', subsync: 'Subtitle Sync', @@ -447,7 +471,7 @@ function topSection(path: string): string { yomitan: 'Yomitan', youtube: 'YouTube Playback Settings', youtubeSubgen: 'YouTube subtitle generation', - auto_start_overlay: 'Visible Overlay Auto-Start', + auto_start_overlay: 'Playback Behavior', }; return labels[top] ?? humanizePath(top); } @@ -515,9 +539,11 @@ function subsectionForPath(path: string): string | undefined { if (path === 'stats.toggleKey' || path === 'stats.markWatchedKey') { return 'Toggle & Visibility'; } + if (path === 'mpv.aniskipButtonKey') { + return 'Playback'; + } if (path.startsWith('shortcuts.')) { const leaf = path.split('.').at(-1) ?? ''; - if (leaf === 'multiCopyTimeoutMs') return 'Timing'; if ( leaf === 'copySubtitle' || leaf === 'copySubtitleMultiple' || @@ -632,6 +658,7 @@ function restartBehaviorForPath(path: string): ConfigSettingsRestartBehavior { path === 'ankiConnect.fields.miscInfo' || path === 'ankiConnect.isLapis.sentenceCardModel' || path === 'ankiConnect.isKiku.fieldGrouping' || + path === 'mpv.aniskipButtonKey' || path === 'stats.toggleKey' || path === 'stats.markWatchedKey' || path === 'logging.level' || diff --git a/src/core/services/app-lifecycle.test.ts b/src/core/services/app-lifecycle.test.ts index b050414c..ff5bd276 100644 --- a/src/core/services/app-lifecycle.test.ts +++ b/src/core/services/app-lifecycle.test.ts @@ -14,8 +14,8 @@ function makeArgs(overrides: Partial = {}): CliArgs { toggle: false, toggleVisibleOverlay: false, togglePrimarySubtitleBar: false, + yomitan: false, settings: false, - configSettings: false, setup: false, show: false, hide: false, @@ -223,3 +223,22 @@ test('startAppLifecycle queues second-instance commands until app ready runtime runSecondInstance(['SubMiner', '--start']); assert.deepEqual(handled, ['ready', 'second-instance:start', 'second-instance:start']); }); + +test('startAppLifecycle quits macOS config-only launch when all windows close', () => { + let windowAllClosedHandler: (() => void) | null = null; + const { deps, calls } = createDeps({ + shouldStartApp: () => true, + isDarwinPlatform: () => true, + shouldQuitOnWindowAllClosed: () => true, + onWindowAllClosed: (handler) => { + windowAllClosedHandler = handler; + }, + }); + + startAppLifecycle(makeArgs({ settings: true }), deps); + + const handler = windowAllClosedHandler as (() => void) | null; + assert.ok(handler); + handler(); + assert.deepEqual(calls, ['quitApp']); +}); diff --git a/src/core/services/app-lifecycle.ts b/src/core/services/app-lifecycle.ts index 74028d39..179e779c 100644 --- a/src/core/services/app-lifecycle.ts +++ b/src/core/services/app-lifecycle.ts @@ -164,7 +164,10 @@ export function startAppLifecycle(initialArgs: CliArgs, deps: AppLifecycleServic }); deps.onWindowAllClosed(() => { - if (!deps.isDarwinPlatform() && deps.shouldQuitOnWindowAllClosed()) { + if ( + deps.shouldQuitOnWindowAllClosed() && + (!deps.isDarwinPlatform() || initialArgs.settings) + ) { deps.quitApp(); } }); diff --git a/src/core/services/cli-command.test.ts b/src/core/services/cli-command.test.ts index c11583b2..07f28d0c 100644 --- a/src/core/services/cli-command.test.ts +++ b/src/core/services/cli-command.test.ts @@ -15,8 +15,8 @@ function makeArgs(overrides: Partial = {}): CliArgs { stop: false, toggle: false, toggleVisibleOverlay: false, + yomitan: false, settings: false, - configSettings: false, setup: false, show: false, hide: false, @@ -586,8 +586,8 @@ test('handleCliCommand handles visibility and utility command dispatches', () => args: Partial; expected: string; }> = [ - { args: { settings: true }, expected: 'openYomitanSettingsDelayed:1000' }, - { args: { configSettings: true }, expected: 'openConfigSettingsWindow' }, + { args: { yomitan: true }, expected: 'openYomitanSettingsDelayed:1000' }, + { args: { settings: true }, expected: 'openConfigSettingsWindow' }, { args: { showVisibleOverlay: true }, expected: 'setVisibleOverlayVisible:true', diff --git a/src/core/services/cli-command.ts b/src/core/services/cli-command.ts index 2ce23238..5cf22329 100644 --- a/src/core/services/cli-command.ts +++ b/src/core/services/cli-command.ts @@ -386,9 +386,9 @@ export function handleCliCommand( } else if (args.setup) { deps.openFirstRunSetup(true); deps.logDebug('Opened first-run setup flow.'); - } else if (args.settings) { + } else if (args.yomitan) { deps.openYomitanSettingsDelayed(1000); - } else if (args.configSettings) { + } else if (args.settings) { deps.openConfigSettingsWindow(); } else if (args.show || args.showVisibleOverlay) { deps.setVisibleOverlayVisible(true); diff --git a/src/core/services/config-hot-reload.test.ts b/src/core/services/config-hot-reload.test.ts index e62b2b47..1175afa3 100644 --- a/src/core/services/config-hot-reload.test.ts +++ b/src/core/services/config-hot-reload.test.ts @@ -21,6 +21,7 @@ test('classifyConfigHotReloadDiff separates hot and restart-required fields', () test('classifyConfigHotReloadDiff treats safe nested config paths as hot-reloadable', () => { const prev = deepCloneConfig(DEFAULT_CONFIG); const next = deepCloneConfig(DEFAULT_CONFIG); + next.mpv.aniskipButtonKey = 'F8'; next.stats.toggleKey = 'F8'; next.stats.markWatchedKey = 'F9'; next.logging.level = 'debug'; @@ -52,6 +53,7 @@ test('classifyConfigHotReloadDiff treats safe nested config paths as hot-reloada new Set(diff.hotReloadFields), new Set([ 'stats.toggleKey', + 'mpv.aniskipButtonKey', 'stats.markWatchedKey', 'logging.level', 'youtube.primarySubLanguages', diff --git a/src/core/services/config-hot-reload.ts b/src/core/services/config-hot-reload.ts index 14a9a3f9..46ae6357 100644 --- a/src/core/services/config-hot-reload.ts +++ b/src/core/services/config-hot-reload.ts @@ -56,6 +56,7 @@ const HOT_RELOAD_ROOTS = ['subtitleStyle', 'keybindings', 'shortcuts', 'subtitle const HOT_RELOAD_EXACT_OR_PREFIX_PATHS = [ 'secondarySub.defaultMode', + 'mpv.aniskipButtonKey', 'ankiConnect.ai.enabled', 'stats.toggleKey', 'stats.markWatchedKey', diff --git a/src/core/services/startup-bootstrap.test.ts b/src/core/services/startup-bootstrap.test.ts index a8dd8e8a..afd2599a 100644 --- a/src/core/services/startup-bootstrap.test.ts +++ b/src/core/services/startup-bootstrap.test.ts @@ -14,8 +14,8 @@ function makeArgs(overrides: Partial = {}): CliArgs { toggle: false, toggleVisibleOverlay: false, togglePrimarySubtitleBar: false, + yomitan: false, settings: false, - configSettings: false, setup: false, show: false, hide: false, diff --git a/src/main.ts b/src/main.ts index 97db301f..8321e1dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,7 +21,6 @@ import { clipboard, globalShortcut, ipcMain, - net, shell, protocol, Extension, @@ -91,7 +90,7 @@ protocol.registerSchemesAsPrivileged([ ]); import * as fs from 'fs'; -import { spawn } from 'node:child_process'; +import { execFile, spawn } from 'node:child_process'; import * as os from 'os'; import * as path from 'path'; import { MecabTokenizer } from './mecab-tokenizer'; @@ -505,11 +504,7 @@ import { createElectronAppUpdater, isNativeUpdaterSupported, } from './main/runtime/update/app-updater'; -import { - createCurlFetch, - createElectronNetFetch, - createGlobalFetch, -} from './main/runtime/update/fetch-adapter'; +import { createCurlFetch, createGlobalFetch } from './main/runtime/update/fetch-adapter'; import { createCurlHttpExecutor } from './main/runtime/update/curl-http-executor'; import { createFetchHttpExecutor } from './main/runtime/update/fetch-http-executor'; import { @@ -618,6 +613,7 @@ const ANILIST_UPDATE_MIN_WATCH_SECONDS = 10 * 60; const ANILIST_DURATION_RETRY_INTERVAL_MS = 15_000; const ANILIST_MAX_ATTEMPTED_UPDATE_KEYS = 1000; const TRAY_TOOLTIP = 'SubMiner'; +const SUBMINER_BUNDLE_ID = 'com.sudacode.SubMiner'; const JELLYFIN_SETUP_PRELOAD_PATH = path.join(__dirname, 'preload-jellyfin-setup.js'); let anilistMediaGuessRuntimeState: AnilistMediaGuessRuntimeState = @@ -4894,28 +4890,19 @@ flushPendingMpvLogWrites = () => { const updateStateStore = createFileUpdateStateStore(path.join(USER_DATA_PATH, 'update-state.json')); let updateService: ReturnType | null = null; -const electronNetFetch = createElectronNetFetch({ - fetch: (url, init) => net.fetch(url, init as RequestInit), -}); const globalFetchForUpdater = createGlobalFetch(); const curlFetch = createCurlFetch(); function createNativeUpdaterHttpExecutor() { - if (process.platform === 'darwin') { - return createCurlHttpExecutor(); - } if (process.platform === 'win32') { return createFetchHttpExecutor(); } - return undefined; + return createCurlHttpExecutor(); } function getFetchForUpdater() { - if (process.platform === 'win32') { - return globalFetchForUpdater; - } - if (process.platform === 'linux') return curlFetch; - return electronNetFetch; + if (process.platform === 'win32') return globalFetchForUpdater; + return curlFetch; } async function updateLauncherFromSelectedRelease( @@ -4962,11 +4949,8 @@ function getUpdateService() { isPackaged: app.isPackaged, log: (message) => logger.info(message), getChannel: () => getResolvedConfig().updates.channel, - configureHttpExecutor: - process.platform === 'darwin' || process.platform === 'win32' - ? createNativeUpdaterHttpExecutor - : undefined, - disableDifferentialDownload: process.platform === 'darwin' || process.platform === 'win32', + configureHttpExecutor: createNativeUpdaterHttpExecutor, + disableDifferentialDownload: true, isNativeUpdaterSupported: () => isNativeUpdaterSupported({ platform: process.platform, @@ -4978,7 +4962,37 @@ function getUpdateService() { }); const updateDialogPresenter = createUpdateDialogPresenter({ platform: process.platform, - focusApp: () => app.focus({ steal: true }), + focusApp: async () => { + if (process.platform !== 'darwin') { + app.focus({ steal: true }); + return; + } + try { + await app.dock?.show(); + } catch (error) { + logger.warn('Failed to show macOS dock before update dialog', error); + } + // app.focus({ steal: true }) alone does not reliably activate the process + // when SubMiner was reached via `subminer -u` (single-instance forwarding + // from a CLI-spawned child). osascript's `activate` uses LaunchServices, + // which is the only path that reliably brings the running app forward. + await new Promise((resolve) => { + execFile( + '/usr/bin/osascript', + ['-e', `tell application id "${SUBMINER_BUNDLE_ID}" to activate`], + { timeout: 2000 }, + (error) => { + if (error) { + logger.warn( + `Failed to activate SubMiner via osascript: ${error instanceof Error ? error.message : String(error)}`, + ); + } + resolve(); + }, + ); + }); + app.focus({ steal: true }); + }, showMessageBox: (options) => dialog.showMessageBox(options), }); updateService = createUpdateService({ diff --git a/src/main/runtime/config-settings-ipc.test.ts b/src/main/runtime/config-settings-ipc.test.ts index 2dd9f2e5..f35772ad 100644 --- a/src/main/runtime/config-settings-ipc.test.ts +++ b/src/main/runtime/config-settings-ipc.test.ts @@ -10,7 +10,7 @@ const fields: ConfigSettingsField[] = [ description: 'Launch mode setting.', configPath: 'mpv.launchMode', category: 'behavior', - section: 'MPV Launcher', + section: 'mpv Playback', control: 'select', defaultValue: 'windowed', restartBehavior: 'restart', diff --git a/src/main/runtime/config-settings-window.ts b/src/main/runtime/config-settings-window.ts index f5804b81..a3167291 100644 --- a/src/main/runtime/config-settings-window.ts +++ b/src/main/runtime/config-settings-window.ts @@ -27,7 +27,7 @@ export function createOpenConfigSettingsWindowHandler { const message = error instanceof Error ? error.message : String(error); - deps.log?.(`Failed to load configuration settings window: ${message}`); + deps.log?.(`Failed to load settings window: ${message}`); deps.setSettingsWindow(null); window.destroy?.(); }); diff --git a/src/main/runtime/first-run-setup-service.test.ts b/src/main/runtime/first-run-setup-service.test.ts index d9c1cc0a..3a13e253 100644 --- a/src/main/runtime/first-run-setup-service.test.ts +++ b/src/main/runtime/first-run-setup-service.test.ts @@ -29,8 +29,8 @@ function makeArgs(overrides: Partial = {}): CliArgs { toggle: false, toggleVisibleOverlay: false, togglePrimarySubtitleBar: false, + yomitan: false, settings: false, - configSettings: false, setup: false, show: false, hide: false, @@ -122,12 +122,12 @@ function createCommandLineLauncherSnapshot( test('shouldAutoOpenFirstRunSetup only for startup/setup intents', () => { assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, background: true })), true); assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ background: true, setup: true })), true); - assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, configSettings: true })), false); + assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, settings: true })), false); assert.equal( shouldAutoOpenFirstRunSetup(makeArgs({ background: true, jellyfinRemoteAnnounce: true })), false, ); - assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ settings: true })), false); + assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ yomitan: true })), false); assert.equal(shouldAutoOpenFirstRunSetup(makeArgs({ start: true, update: true })), false); }); diff --git a/src/main/runtime/first-run-setup-service.ts b/src/main/runtime/first-run-setup-service.ts index 6e28beaa..b6a3276a 100644 --- a/src/main/runtime/first-run-setup-service.ts +++ b/src/main/runtime/first-run-setup-service.ts @@ -71,8 +71,8 @@ function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean { args.toggleVisibleOverlay || args.togglePrimarySubtitleBar || args.launchMpv || + args.yomitan || args.settings || - args.configSettings || args.show || args.hide || args.showVisibleOverlay || diff --git a/src/main/runtime/setup-window-factory.test.ts b/src/main/runtime/setup-window-factory.test.ts index bc02a77f..e323c3de 100644 --- a/src/main/runtime/setup-window-factory.test.ts +++ b/src/main/runtime/setup-window-factory.test.ts @@ -110,7 +110,7 @@ test('createCreateConfigSettingsWindowHandler builds configuration settings wind assert.deepEqual(options, { width: 1040, height: 760, - title: 'SubMiner Configuration', + title: 'SubMiner Settings', show: true, autoHideMenuBar: true, resizable: true, diff --git a/src/main/runtime/setup-window-factory.ts b/src/main/runtime/setup-window-factory.ts index 4f469172..02ac42a7 100644 --- a/src/main/runtime/setup-window-factory.ts +++ b/src/main/runtime/setup-window-factory.ts @@ -76,7 +76,7 @@ export function createCreateConfigSettingsWindowHandler(deps: { return createSetupWindowHandler(deps, { width: 1040, height: 760, - title: 'SubMiner Configuration', + title: 'SubMiner Settings', resizable: true, preloadPath: deps.preloadPath, backgroundColor: '#24273a', diff --git a/src/main/runtime/startup-mode-flags.test.ts b/src/main/runtime/startup-mode-flags.test.ts index 6bb8dd91..95961bab 100644 --- a/src/main/runtime/startup-mode-flags.test.ts +++ b/src/main/runtime/startup-mode-flags.test.ts @@ -7,8 +7,8 @@ import { shouldStartAutomaticUpdateChecks, } from './startup-mode-flags'; -test('config settings startup uses minimal startup and skips background integrations', () => { - const args = parseArgs(['--config']); +test('settings window startup uses minimal startup and skips background integrations', () => { + const args = parseArgs(['--settings']); const flags = getStartupModeFlags(args); assert.equal(flags.shouldUseMinimalStartup, true); diff --git a/src/main/runtime/startup-mode-flags.ts b/src/main/runtime/startup-mode-flags.ts index 2d9a015f..86a468ee 100644 --- a/src/main/runtime/startup-mode-flags.ts +++ b/src/main/runtime/startup-mode-flags.ts @@ -2,7 +2,7 @@ import type { CliArgs } from '../../cli/args'; import { isHeadlessInitialCommand, isStandaloneTexthookerCommand, - shouldRunSettingsOnlyStartup, + shouldRunYomitanOnlyStartup, } from '../../cli/args'; export function getStartupModeFlags(initialArgs: CliArgs | null | undefined): { @@ -12,15 +12,15 @@ export function getStartupModeFlags(initialArgs: CliArgs | null | undefined): { return { shouldUseMinimalStartup: Boolean( (initialArgs && isStandaloneTexthookerCommand(initialArgs)) || - initialArgs?.configSettings || + initialArgs?.settings || initialArgs?.update || (initialArgs?.stats && (initialArgs.statsCleanup || initialArgs.statsBackground || initialArgs.statsStop)), ), shouldSkipHeavyStartup: Boolean( initialArgs && - (shouldRunSettingsOnlyStartup(initialArgs) || - initialArgs.configSettings || + (shouldRunYomitanOnlyStartup(initialArgs) || + initialArgs.settings || initialArgs.stats || initialArgs.dictionary || initialArgs.update || @@ -32,9 +32,9 @@ export function getStartupModeFlags(initialArgs: CliArgs | null | undefined): { export function shouldRefreshAnilistOnConfigReload( initialArgs: CliArgs | null | undefined, ): boolean { - return !(initialArgs && (isHeadlessInitialCommand(initialArgs) || initialArgs.configSettings)); + return !(initialArgs && (isHeadlessInitialCommand(initialArgs) || initialArgs.settings)); } export function shouldStartAutomaticUpdateChecks(initialArgs: CliArgs | null | undefined): boolean { - return !(initialArgs && (isHeadlessInitialCommand(initialArgs) || initialArgs.configSettings)); + return !(initialArgs && (isHeadlessInitialCommand(initialArgs) || initialArgs.settings)); } diff --git a/src/main/runtime/tray-runtime.ts b/src/main/runtime/tray-runtime.ts index 12afa864..98c2443d 100644 --- a/src/main/runtime/tray-runtime.ts +++ b/src/main/runtime/tray-runtime.ts @@ -94,7 +94,7 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers): click: handlers.openRuntimeOptions, }, { - label: 'Open Configuration', + label: 'Open Settings', click: handlers.openConfigSettings, }, { diff --git a/src/main/runtime/update/app-updater.test.ts b/src/main/runtime/update/app-updater.test.ts index a1f73623..54febc04 100644 --- a/src/main/runtime/update/app-updater.test.ts +++ b/src/main/runtime/update/app-updater.test.ts @@ -258,7 +258,7 @@ test('mac native updater supports Developer ID signed packaged app bundles', asy assert.deepEqual(logged, []); }); -test('linux native updater is unsupported even for writable direct AppImage installs', async () => { +test('linux native updater is supported for direct AppImage installs', async () => { const logged: string[] = []; const supported = await isNativeUpdaterSupported({ platform: 'linux', @@ -270,10 +270,8 @@ test('linux native updater is unsupported even for writable direct AppImage inst log: (message) => logged.push(message), }); - assert.equal(supported, false); - assert.deepEqual(logged, [ - 'Skipping native Linux updater because Linux tray checks use GitHub release assets.', - ]); + assert.equal(supported, true); + assert.deepEqual(logged, []); }); test('linux native updater is unsupported when APPIMAGE is missing', async () => { @@ -288,25 +286,7 @@ test('linux native updater is unsupported when APPIMAGE is missing', async () => assert.equal(supported, false); assert.deepEqual(logged, [ - 'Skipping native Linux updater because Linux tray checks use GitHub release assets.', - ]); -}); - -test('linux native updater is unsupported for non-writable AppImage installs', async () => { - const logged: string[] = []; - const supported = await isNativeUpdaterSupported({ - platform: 'linux', - isPackaged: true, - execPath: '/tmp/.mount_SubMiner/SubMiner', - env: { - APPIMAGE: '/home/tester/.local/bin/SubMiner.AppImage', - }, - log: (message) => logged.push(message), - }); - - assert.equal(supported, false); - assert.deepEqual(logged, [ - 'Skipping native Linux updater because Linux tray checks use GitHub release assets.', + 'Skipping native Linux updater because APPIMAGE is not set (not launched from an AppImage).', ]); }); @@ -324,7 +304,7 @@ test('linux native updater is unsupported for package-managed AppImage installs' assert.equal(supported, false); assert.deepEqual(logged, [ - 'Skipping native Linux updater because Linux tray checks use GitHub release assets.', + 'Skipping native Linux updater because the AppImage is managed by a system package.', ]); }); diff --git a/src/main/runtime/update/app-updater.ts b/src/main/runtime/update/app-updater.ts index 21014762..3b8c4098 100644 --- a/src/main/runtime/update/app-updater.ts +++ b/src/main/runtime/update/app-updater.ts @@ -108,15 +108,25 @@ export async function isNativeUpdaterSupported(options: { options.log?.('Skipping native updater because this build is not packaged.'); return false; } - if (options.platform === 'linux') { - options.log?.( - 'Skipping native Linux updater because Linux tray checks use GitHub release assets.', - ); - return false; - } if (options.platform === 'win32') { return true; } + if (options.platform === 'linux') { + const appImagePath = options.env?.APPIMAGE; + if (!appImagePath) { + options.log?.( + 'Skipping native Linux updater because APPIMAGE is not set (not launched from an AppImage).', + ); + return false; + } + if (isKnownLinuxPackageManagedAppImage(appImagePath)) { + options.log?.( + 'Skipping native Linux updater because the AppImage is managed by a system package.', + ); + return false; + } + return true; + } if (options.platform !== 'darwin') { options.log?.('Skipping native updater because this platform uses GitHub metadata checks.'); return false; diff --git a/src/main/runtime/update/update-dialogs.test.ts b/src/main/runtime/update/update-dialogs.test.ts index 4039dfe1..b4625290 100644 --- a/src/main/runtime/update/update-dialogs.test.ts +++ b/src/main/runtime/update/update-dialogs.test.ts @@ -6,7 +6,7 @@ import { type ShowMessageBox, } from './update-dialogs'; -test('update dialog presenter focuses app before showing macOS dialogs', async () => { +test('update dialog presenter focuses app and yields the run loop before showing macOS dialogs', async () => { const calls: string[] = []; const showMessageBox: ShowMessageBox = async (options) => { calls.push(`dialog:${options.message}`); @@ -14,16 +14,44 @@ test('update dialog presenter focuses app before showing macOS dialogs', async ( }; const presenter = createUpdateDialogPresenter({ platform: 'darwin', - focusApp: () => calls.push('focus'), + focusApp: () => { + calls.push('focus'); + }, + yieldToRunLoop: async () => { + calls.push('yield'); + }, showMessageBox, }); await presenter.showNoUpdateDialog('0.14.0'); - assert.deepEqual(calls, ['focus', 'dialog:SubMiner is up to date (v0.14.0)']); + assert.deepEqual(calls, ['focus', 'yield', 'dialog:SubMiner is up to date (v0.14.0)']); }); -test('update dialog presenter does not focus app before showing non-macOS dialogs', async () => { +test('update dialog presenter awaits async focusApp before yielding and showing the dialog', async () => { + const calls: string[] = []; + const showMessageBox: ShowMessageBox = async (options) => { + calls.push(`dialog:${options.message}`); + return { response: 0 }; + }; + const presenter = createUpdateDialogPresenter({ + platform: 'darwin', + focusApp: async () => { + await new Promise((resolve) => setImmediate(resolve)); + calls.push('focus'); + }, + yieldToRunLoop: async () => { + calls.push('yield'); + }, + showMessageBox, + }); + + await presenter.showNoUpdateDialog('0.14.0'); + + assert.deepEqual(calls, ['focus', 'yield', 'dialog:SubMiner is up to date (v0.14.0)']); +}); + +test('update dialog presenter does not focus app or yield before showing non-macOS dialogs', async () => { const calls: string[] = []; const showMessageBox: ShowMessageBox = async (options) => { calls.push(`dialog:${options.message}`); @@ -31,7 +59,12 @@ test('update dialog presenter does not focus app before showing non-macOS dialog }; const presenter = createUpdateDialogPresenter({ platform: 'linux', - focusApp: () => calls.push('focus'), + focusApp: () => { + calls.push('focus'); + }, + yieldToRunLoop: async () => { + calls.push('yield'); + }, showMessageBox, }); diff --git a/src/main/runtime/update/update-dialogs.ts b/src/main/runtime/update/update-dialogs.ts index 01e07275..c53b390e 100644 --- a/src/main/runtime/update/update-dialogs.ts +++ b/src/main/runtime/update/update-dialogs.ts @@ -17,7 +17,8 @@ export type ShowMessageBox = (options: { export interface UpdateDialogPresenterDeps { showMessageBox: ShowMessageBox; - focusApp?: () => void; + focusApp?: () => void | Promise; + yieldToRunLoop?: () => Promise; platform?: NodeJS.Platform; } @@ -33,14 +34,19 @@ export async function showNoUpdateDialog( }); } -function maybeFocusAppForDialog(deps: UpdateDialogPresenterDeps): void { +async function maybeFocusAppForDialog(deps: UpdateDialogPresenterDeps): Promise { if ((deps.platform ?? process.platform) !== 'darwin') return; - deps.focusApp?.(); + await deps.focusApp?.(); + // Yield to the macOS run loop so the activation request is processed before the + // modal alert blocks JS execution; without this, the alert often appears behind + // other apps when SubMiner is not the active app at dialog-show time. + const yieldToRunLoop = deps.yieldToRunLoop ?? (() => new Promise((r) => setTimeout(r, 0))); + await yieldToRunLoop(); } export function createUpdateDialogPresenter(deps: UpdateDialogPresenterDeps) { const showFocusedMessageBox: ShowMessageBox = async (options) => { - maybeFocusAppForDialog(deps); + await maybeFocusAppForDialog(deps); return deps.showMessageBox(options); }; diff --git a/src/settings/index.html b/src/settings/index.html index 31bafc3a..d86baecc 100644 --- a/src/settings/index.html +++ b/src/settings/index.html @@ -7,22 +7,22 @@ http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self';" /> - SubMiner Configuration + SubMiner Settings
-