mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
refactor: extract main runtime lifecycle helper builders
This commit is contained in:
@@ -6,7 +6,7 @@ Read first. Keep concise.
|
|||||||
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
||||||
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
||||||
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
||||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` |
|
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T03:27:35Z` |
|
||||||
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
||||||
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
||||||
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
|
|
||||||
## Current Work (newest first)
|
## Current Work (newest first)
|
||||||
|
|
||||||
|
- [2026-02-20T03:27:35Z] progress: extracted CLI context deps builder into `src/main/runtime/cli-command-context-deps.ts` (`createBuildCliCommandContextDepsHandler`) and rewired `handleCliCommand` to build deps through the helper.
|
||||||
|
- [2026-02-20T03:27:35Z] progress: extracted overlay runtime options builder into `src/main/runtime/overlay-runtime-options.ts` (`createBuildInitializeOverlayRuntimeOptionsHandler`) and rewired `initializeOverlayRuntime` `buildOptions`; extracted Yomitan extension load wrappers into `src/main/runtime/yomitan-extension-loader.ts` (`createLoadYomitanExtensionHandler`, `createEnsureYomitanExtensionLoadedHandler`) and rewired `loadYomitanExtension` + `ensureYomitanExtensionLoaded`.
|
||||||
|
- [2026-02-20T03:27:35Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/cli-command-context-deps.test.js dist/main/runtime/overlay-runtime-options.test.js dist/main/runtime/yomitan-extension-loader.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (116/116).
|
||||||
|
- [2026-02-20T03:19:44Z] progress: extracted tray wrappers into `src/main/runtime/tray-main-actions.ts` (`createResolveTrayIconPathHandler`, `createBuildTrayMenuTemplateHandler`) and rewired `resolveTrayIconPath` + `buildTrayMenu`.
|
||||||
|
- [2026-02-20T03:19:44Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/tray-main-actions.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (110/110).
|
||||||
|
- [2026-02-20T03:17:35Z] progress: extracted overlay window creation wrappers into `src/main/runtime/overlay-window-factory.ts` (`createCreateOverlayWindowHandler`, `createCreateMainWindowHandler`, `createCreateInvisibleWindowHandler`) and rewired `createOverlayWindow`, `createMainWindow`, `createInvisibleWindow`.
|
||||||
|
- [2026-02-20T03:17:35Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (108/108).
|
||||||
|
- [2026-02-20T03:14:23Z] progress: extracted thin wrapper cluster into `src/main/runtime/overlay-main-actions.ts` (`createSetOverlayVisibleHandler`, `createToggleOverlayHandler`, `createHandleOverlayModalClosedHandler`, `createAppendClipboardVideoToQueueHandler`) and rewired `setOverlayVisible`, `toggleOverlay`, `handleOverlayModalClosed`, `appendClipboardVideoToQueue`.
|
||||||
|
- [2026-02-20T03:14:23Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (103/103).
|
||||||
|
- [2026-02-20T03:11:11Z] progress: extracted overlay visibility set/toggle wrappers into `src/main/runtime/overlay-visibility-actions.ts` (`createSetVisibleOverlayVisibleHandler`, `createSetInvisibleOverlayVisibleHandler`, `createToggleVisibleOverlayHandler`, `createToggleInvisibleOverlayHandler`) and rewired corresponding `main.ts` functions.
|
||||||
|
- [2026-02-20T03:11:11Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (99/99).
|
||||||
- [2026-02-20T02:56:34Z] progress: extracted subtitle copy/mine wrapper cluster into `src/main/runtime/mining-actions.ts` (`createHandleMultiCopyDigitHandler`, `createCopyCurrentSubtitleHandler`, `createHandleMineSentenceDigitHandler`) and rewired `handleMultiCopyDigit`, `copyCurrentSubtitle`, `handleMineSentenceDigit`.
|
- [2026-02-20T02:56:34Z] progress: extracted subtitle copy/mine wrapper cluster into `src/main/runtime/mining-actions.ts` (`createHandleMultiCopyDigitHandler`, `createCopyCurrentSubtitleHandler`, `createHandleMineSentenceDigitHandler`) and rewired `handleMultiCopyDigit`, `copyCurrentSubtitle`, `handleMineSentenceDigit`.
|
||||||
- [2026-02-20T02:56:34Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (95/95).
|
- [2026-02-20T02:56:34Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (95/95).
|
||||||
- [2026-02-20T02:55:17Z] progress: extracted Anki action wrappers into `src/main/runtime/anki-actions.ts` (`createUpdateLastCardFromClipboardHandler`, `createRefreshKnownWordCacheHandler`, `createTriggerFieldGroupingHandler`, `createMarkLastCardAsAudioCardHandler`, `createMineSentenceCardHandler`) and rewired `main.ts` handlers.
|
- [2026-02-20T02:55:17Z] progress: extracted Anki action wrappers into `src/main/runtime/anki-actions.ts` (`createUpdateLastCardFromClipboardHandler`, `createRefreshKnownWordCacheHandler`, `createTriggerFieldGroupingHandler`, `createMarkLastCardAsAudioCardHandler`, `createMineSentenceCardHandler`) and rewired `main.ts` handlers.
|
||||||
@@ -130,6 +141,22 @@
|
|||||||
- `src/main/runtime/anki-actions.test.ts`
|
- `src/main/runtime/anki-actions.test.ts`
|
||||||
- `src/main/runtime/mining-actions.ts`
|
- `src/main/runtime/mining-actions.ts`
|
||||||
- `src/main/runtime/mining-actions.test.ts`
|
- `src/main/runtime/mining-actions.test.ts`
|
||||||
|
- `src/main/runtime/overlay-visibility-actions.ts`
|
||||||
|
- `src/main/runtime/overlay-visibility-actions.test.ts`
|
||||||
|
- `src/main/runtime/overlay-main-actions.ts`
|
||||||
|
- `src/main/runtime/overlay-main-actions.test.ts`
|
||||||
|
- `src/main/runtime/ipc-bridge-actions.ts`
|
||||||
|
- `src/main/runtime/ipc-bridge-actions.test.ts`
|
||||||
|
- `src/main/runtime/overlay-window-factory.ts`
|
||||||
|
- `src/main/runtime/overlay-window-factory.test.ts`
|
||||||
|
- `src/main/runtime/tray-main-actions.ts`
|
||||||
|
- `src/main/runtime/tray-main-actions.test.ts`
|
||||||
|
- `src/main/runtime/yomitan-extension-loader.ts`
|
||||||
|
- `src/main/runtime/yomitan-extension-loader.test.ts`
|
||||||
|
- `src/main/runtime/overlay-runtime-options.ts`
|
||||||
|
- `src/main/runtime/overlay-runtime-options.test.ts`
|
||||||
|
- `src/main/runtime/cli-command-context-deps.ts`
|
||||||
|
- `src/main/runtime/cli-command-context-deps.test.ts`
|
||||||
- `src/main/runtime/anilist-token-refresh.ts`
|
- `src/main/runtime/anilist-token-refresh.ts`
|
||||||
- `src/main/runtime/anilist-token-refresh.test.ts`
|
- `src/main/runtime/anilist-token-refresh.test.ts`
|
||||||
- `src/main/runtime/anilist-media-guess.ts`
|
- `src/main/runtime/anilist-media-guess.ts`
|
||||||
@@ -161,4 +188,4 @@
|
|||||||
|
|
||||||
## Next Step
|
## Next Step
|
||||||
|
|
||||||
- extract next `src/main.ts` overlay visibility state wrappers (`setVisibleOverlayVisible`, `setInvisibleOverlayVisible`, `toggleVisibleOverlay`, `toggleInvisibleOverlay`) into runtime helper + tests.
|
- extract next `src/main.ts` app-lifecycle cleanup/restore wrapper cluster in startup lifecycle wiring (`onWillQuitCleanup`, `restoreWindowsOnActivate`) into runtime helper + tests.
|
||||||
|
|||||||
541
src/main.ts
541
src/main.ts
@@ -208,6 +208,37 @@ import {
|
|||||||
createHandleMineSentenceDigitHandler,
|
createHandleMineSentenceDigitHandler,
|
||||||
createHandleMultiCopyDigitHandler,
|
createHandleMultiCopyDigitHandler,
|
||||||
} from './main/runtime/mining-actions';
|
} from './main/runtime/mining-actions';
|
||||||
|
import {
|
||||||
|
createSetInvisibleOverlayVisibleHandler,
|
||||||
|
createSetVisibleOverlayVisibleHandler,
|
||||||
|
createToggleInvisibleOverlayHandler,
|
||||||
|
createToggleVisibleOverlayHandler,
|
||||||
|
} from './main/runtime/overlay-visibility-actions';
|
||||||
|
import {
|
||||||
|
createAppendClipboardVideoToQueueHandler,
|
||||||
|
createHandleOverlayModalClosedHandler,
|
||||||
|
createSetOverlayVisibleHandler,
|
||||||
|
createToggleOverlayHandler,
|
||||||
|
} from './main/runtime/overlay-main-actions';
|
||||||
|
import {
|
||||||
|
createHandleMpvCommandFromIpcHandler,
|
||||||
|
createRunSubsyncManualFromIpcHandler,
|
||||||
|
} from './main/runtime/ipc-bridge-actions';
|
||||||
|
import {
|
||||||
|
createCreateInvisibleWindowHandler,
|
||||||
|
createCreateMainWindowHandler,
|
||||||
|
createCreateOverlayWindowHandler,
|
||||||
|
} from './main/runtime/overlay-window-factory';
|
||||||
|
import {
|
||||||
|
createBuildTrayMenuTemplateHandler,
|
||||||
|
createResolveTrayIconPathHandler,
|
||||||
|
} from './main/runtime/tray-main-actions';
|
||||||
|
import {
|
||||||
|
createEnsureYomitanExtensionLoadedHandler,
|
||||||
|
createLoadYomitanExtensionHandler,
|
||||||
|
} from './main/runtime/yomitan-extension-loader';
|
||||||
|
import { createBuildInitializeOverlayRuntimeOptionsHandler } from './main/runtime/overlay-runtime-options';
|
||||||
|
import { createBuildCliCommandContextDepsHandler } from './main/runtime/cli-command-context-deps';
|
||||||
import {
|
import {
|
||||||
buildRestartRequiredConfigMessage,
|
buildRestartRequiredConfigMessage,
|
||||||
createConfigHotReloadAppliedHandler,
|
createConfigHotReloadAppliedHandler,
|
||||||
@@ -1926,57 +1957,7 @@ function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'):
|
|||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
})(args);
|
})(args);
|
||||||
|
|
||||||
const cliContext = createCliCommandContext({
|
const cliContext = createCliCommandContext(buildCliCommandContextDepsHandler());
|
||||||
getSocketPath: () => appState.mpvSocketPath,
|
|
||||||
setSocketPath: (socketPath: string) => {
|
|
||||||
appState.mpvSocketPath = socketPath;
|
|
||||||
},
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
showOsd: (text: string) => showMpvOsd(text),
|
|
||||||
texthookerService,
|
|
||||||
getTexthookerPort: () => appState.texthookerPort,
|
|
||||||
setTexthookerPort: (port: number) => {
|
|
||||||
appState.texthookerPort = port;
|
|
||||||
},
|
|
||||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
|
||||||
openExternal: (url: string) => shell.openExternal(url),
|
|
||||||
logBrowserOpenError: (url: string, error: unknown) =>
|
|
||||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
|
||||||
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
|
||||||
initializeOverlay: () => initializeOverlayRuntime(),
|
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
|
||||||
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
|
||||||
setVisibleOverlay: (visible: boolean) => setVisibleOverlayVisible(visible),
|
|
||||||
setInvisibleOverlay: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
|
||||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
|
||||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
|
||||||
mineSentenceCard: () => mineSentenceCard(),
|
|
||||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
|
||||||
startPendingMineSentenceMultiple(timeoutMs),
|
|
||||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
|
||||||
refreshKnownWordCache: () => refreshKnownWordCache(),
|
|
||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
|
||||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
|
||||||
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
|
||||||
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
|
||||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
|
||||||
openJellyfinSetup: () => openJellyfinSetupWindow(),
|
|
||||||
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
|
||||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
|
||||||
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
|
||||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
|
||||||
stopApp: () => app.quit(),
|
|
||||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
|
||||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
|
||||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
|
||||||
logInfo: (message: string) => logger.info(message),
|
|
||||||
logWarn: (message: string) => logger.warn(message),
|
|
||||||
logError: (message: string, err: unknown) => logger.error(message, err),
|
|
||||||
});
|
|
||||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2213,112 +2194,30 @@ const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadYomitanExtension(): Promise<Extension | null> {
|
async function loadYomitanExtension(): Promise<Extension | null> {
|
||||||
return loadYomitanExtensionCore({
|
return loadYomitanExtensionHandler();
|
||||||
userDataPath: USER_DATA_PATH,
|
|
||||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
|
||||||
setYomitanParserWindow: (window) => {
|
|
||||||
appState.yomitanParserWindow = window;
|
|
||||||
},
|
|
||||||
setYomitanParserReadyPromise: (promise) => {
|
|
||||||
appState.yomitanParserReadyPromise = promise;
|
|
||||||
},
|
|
||||||
setYomitanParserInitPromise: (promise) => {
|
|
||||||
appState.yomitanParserInitPromise = promise;
|
|
||||||
},
|
|
||||||
setYomitanExtension: (extension) => {
|
|
||||||
appState.yomitanExt = extension;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
||||||
if (appState.yomitanExt) {
|
return ensureYomitanExtensionLoadedHandler();
|
||||||
return appState.yomitanExt;
|
|
||||||
}
|
|
||||||
if (yomitanLoadInFlight) {
|
|
||||||
return yomitanLoadInFlight;
|
|
||||||
}
|
|
||||||
|
|
||||||
yomitanLoadInFlight = loadYomitanExtension().finally(() => {
|
|
||||||
yomitanLoadInFlight = null;
|
|
||||||
});
|
|
||||||
return yomitanLoadInFlight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindow(kind: 'visible' | 'invisible'): BrowserWindow {
|
function createOverlayWindow(kind: 'visible' | 'invisible'): BrowserWindow {
|
||||||
return createOverlayWindowCore(kind, {
|
return createOverlayWindowHandler(kind);
|
||||||
isDev,
|
|
||||||
overlayDebugVisualizationEnabled: appState.overlayDebugVisualizationEnabled,
|
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
|
||||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
|
||||||
isOverlayVisible: (windowKind) =>
|
|
||||||
windowKind === 'visible'
|
|
||||||
? overlayManager.getVisibleOverlayVisible()
|
|
||||||
: overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
|
||||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
|
||||||
onWindowClosed: (windowKind) => {
|
|
||||||
if (windowKind === 'visible') {
|
|
||||||
overlayManager.setMainWindow(null);
|
|
||||||
} else {
|
|
||||||
overlayManager.setInvisibleWindow(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createMainWindow(): BrowserWindow {
|
||||||
const window = createOverlayWindow('visible');
|
return createMainWindowHandler();
|
||||||
overlayManager.setMainWindow(window);
|
|
||||||
return window;
|
|
||||||
}
|
}
|
||||||
function createInvisibleWindow(): BrowserWindow {
|
function createInvisibleWindow(): BrowserWindow {
|
||||||
const window = createOverlayWindow('invisible');
|
return createInvisibleWindowHandler();
|
||||||
overlayManager.setInvisibleWindow(window);
|
|
||||||
return window;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTrayIconPath(): string | null {
|
function resolveTrayIconPath(): string | null {
|
||||||
return resolveTrayIconPathRuntime({
|
return resolveTrayIconPathHandler();
|
||||||
platform: process.platform,
|
|
||||||
resourcesPath: process.resourcesPath,
|
|
||||||
appPath: app.getAppPath(),
|
|
||||||
dirname: __dirname,
|
|
||||||
joinPath: (...parts) => path.join(...parts),
|
|
||||||
fileExists: (candidate) => fs.existsSync(candidate),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTrayMenu(): Menu {
|
function buildTrayMenu(): Menu {
|
||||||
return Menu.buildFromTemplate(
|
return Menu.buildFromTemplate(buildTrayMenuTemplateHandler());
|
||||||
buildTrayMenuTemplateRuntime({
|
|
||||||
openOverlay: () => {
|
|
||||||
if (!appState.overlayRuntimeInitialized) {
|
|
||||||
initializeOverlayRuntime();
|
|
||||||
}
|
|
||||||
setVisibleOverlayVisible(true);
|
|
||||||
},
|
|
||||||
openYomitanSettings: () => {
|
|
||||||
openYomitanSettings();
|
|
||||||
},
|
|
||||||
openRuntimeOptions: () => {
|
|
||||||
if (!appState.overlayRuntimeInitialized) {
|
|
||||||
initializeOverlayRuntime();
|
|
||||||
}
|
|
||||||
openRuntimeOptionsPalette();
|
|
||||||
},
|
|
||||||
openJellyfinSetup: () => {
|
|
||||||
openJellyfinSetupWindow();
|
|
||||||
},
|
|
||||||
openAnilistSetup: () => {
|
|
||||||
openAnilistSetupWindow();
|
|
||||||
},
|
|
||||||
quitApp: () => {
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureTray(): void {
|
function ensureTray(): void {
|
||||||
@@ -2357,50 +2256,7 @@ function initializeOverlayRuntime(): void {
|
|||||||
createInitializeOverlayRuntimeHandler({
|
createInitializeOverlayRuntimeHandler({
|
||||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options),
|
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options),
|
||||||
buildOptions: () => ({
|
buildOptions: () => buildInitializeOverlayRuntimeOptionsHandler(),
|
||||||
backendOverride: appState.backendOverride,
|
|
||||||
getInitialInvisibleOverlayVisibility: () =>
|
|
||||||
configDerivedRuntime.getInitialInvisibleOverlayVisibility(),
|
|
||||||
createMainWindow: () => {
|
|
||||||
createMainWindow();
|
|
||||||
},
|
|
||||||
createInvisibleWindow: () => {
|
|
||||||
createInvisibleWindow();
|
|
||||||
},
|
|
||||||
registerGlobalShortcuts: () => {
|
|
||||||
registerGlobalShortcuts();
|
|
||||||
},
|
|
||||||
updateVisibleOverlayBounds: (geometry) => {
|
|
||||||
updateVisibleOverlayBounds(geometry);
|
|
||||||
},
|
|
||||||
updateInvisibleOverlayBounds: (geometry) => {
|
|
||||||
updateInvisibleOverlayBounds(geometry);
|
|
||||||
},
|
|
||||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
isInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
updateVisibleOverlayVisibility: () => {
|
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
|
||||||
},
|
|
||||||
updateInvisibleOverlayVisibility: () => {
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility();
|
|
||||||
},
|
|
||||||
getOverlayWindows: () => getOverlayWindows(),
|
|
||||||
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
|
||||||
setWindowTracker: (tracker) => {
|
|
||||||
appState.windowTracker = tracker;
|
|
||||||
},
|
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
|
||||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
getMpvSocketPath: () => appState.mpvSocketPath,
|
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
|
||||||
setAnkiIntegration: (integration) => {
|
|
||||||
appState.ankiIntegration = integration as AnkiIntegration | null;
|
|
||||||
},
|
|
||||||
showDesktopNotification,
|
|
||||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
|
||||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
|
||||||
}),
|
|
||||||
setInvisibleOverlayVisible: (visible) => {
|
setInvisibleOverlayVisible: (visible) => {
|
||||||
overlayManager.setInvisibleOverlayVisible(visible);
|
overlayManager.setInvisibleOverlayVisible(visible);
|
||||||
},
|
},
|
||||||
@@ -2627,6 +2483,252 @@ const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler({
|
|||||||
},
|
},
|
||||||
handleMineSentenceDigitCore,
|
handleMineSentenceDigitCore,
|
||||||
});
|
});
|
||||||
|
const setVisibleOverlayVisibleHandler = createSetVisibleOverlayVisibleHandler({
|
||||||
|
setVisibleOverlayVisibleCore,
|
||||||
|
setVisibleOverlayVisibleState: (nextVisible) => {
|
||||||
|
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||||
|
},
|
||||||
|
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||||
|
updateInvisibleOverlayVisibility: () => overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||||
|
syncInvisibleOverlayMousePassthrough: () =>
|
||||||
|
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||||
|
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
|
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||||
|
setMpvSubVisibility: (mpvSubVisible) => {
|
||||||
|
setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const setInvisibleOverlayVisibleHandler = createSetInvisibleOverlayVisibleHandler({
|
||||||
|
setInvisibleOverlayVisibleCore,
|
||||||
|
setInvisibleOverlayVisibleState: (nextVisible) => {
|
||||||
|
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
||||||
|
},
|
||||||
|
updateInvisibleOverlayVisibility: () => overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||||
|
syncInvisibleOverlayMousePassthrough: () =>
|
||||||
|
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||||
|
});
|
||||||
|
const toggleVisibleOverlayHandler = createToggleVisibleOverlayHandler({
|
||||||
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
|
});
|
||||||
|
const toggleInvisibleOverlayHandler = createToggleInvisibleOverlayHandler({
|
||||||
|
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
|
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||||
|
});
|
||||||
|
const setOverlayVisibleHandler = createSetOverlayVisibleHandler({
|
||||||
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
|
});
|
||||||
|
const toggleOverlayHandler = createToggleOverlayHandler({
|
||||||
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
|
});
|
||||||
|
const handleOverlayModalClosedHandler = createHandleOverlayModalClosedHandler({
|
||||||
|
handleOverlayModalClosedRuntime: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal),
|
||||||
|
});
|
||||||
|
const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHandler({
|
||||||
|
appendClipboardVideoToQueueRuntime,
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
readClipboardText: () => clipboard.readText(),
|
||||||
|
showMpvOsd: (text) => showMpvOsd(text),
|
||||||
|
sendMpvCommand: (command) => {
|
||||||
|
sendMpvCommandRuntime(appState.mpvClient, command);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const handleMpvCommandFromIpcHandler = createHandleMpvCommandFromIpcHandler({
|
||||||
|
handleMpvCommandFromIpcRuntime,
|
||||||
|
buildMpvCommandDeps: () => ({
|
||||||
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
cycleRuntimeOption: (id, direction) => {
|
||||||
|
if (!appState.runtimeOptionsManager) {
|
||||||
|
return { ok: false, error: 'Runtime options manager unavailable' };
|
||||||
|
}
|
||||||
|
return applyRuntimeOptionResultRuntime(
|
||||||
|
appState.runtimeOptionsManager.cycleOption(id, direction),
|
||||||
|
(text) => showMpvOsd(text),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
|
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||||
|
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
||||||
|
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
||||||
|
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
||||||
|
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||||
|
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const runSubsyncManualFromIpcHandler = createRunSubsyncManualFromIpcHandler({
|
||||||
|
runManualFromIpc: (request: SubsyncManualRunRequest) => subsyncRuntime.runManualFromIpc(request),
|
||||||
|
});
|
||||||
|
const buildCliCommandContextDepsHandler = createBuildCliCommandContextDepsHandler({
|
||||||
|
getSocketPath: () => appState.mpvSocketPath,
|
||||||
|
setSocketPath: (socketPath: string) => {
|
||||||
|
appState.mpvSocketPath = socketPath;
|
||||||
|
},
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
showOsd: (text: string) => showMpvOsd(text),
|
||||||
|
texthookerService,
|
||||||
|
getTexthookerPort: () => appState.texthookerPort,
|
||||||
|
setTexthookerPort: (port: number) => {
|
||||||
|
appState.texthookerPort = port;
|
||||||
|
},
|
||||||
|
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||||
|
openExternal: (url: string) => shell.openExternal(url),
|
||||||
|
logBrowserOpenError: (url: string, error: unknown) =>
|
||||||
|
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
||||||
|
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
initializeOverlay: () => initializeOverlayRuntime(),
|
||||||
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
|
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||||
|
setVisibleOverlay: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
|
setInvisibleOverlay: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||||
|
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||||
|
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||||
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
|
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||||
|
startPendingMineSentenceMultiple(timeoutMs),
|
||||||
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
|
refreshKnownWordCache: () => refreshKnownWordCache(),
|
||||||
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
|
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||||
|
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
||||||
|
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
||||||
|
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||||
|
openJellyfinSetup: () => openJellyfinSetupWindow(),
|
||||||
|
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
||||||
|
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||||
|
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||||
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
|
stopApp: () => app.quit(),
|
||||||
|
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||||
|
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||||
|
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||||
|
logInfo: (message: string) => logger.info(message),
|
||||||
|
logWarn: (message: string) => logger.warn(message),
|
||||||
|
logError: (message: string, err: unknown) => logger.error(message, err),
|
||||||
|
});
|
||||||
|
const createOverlayWindowHandler = createCreateOverlayWindowHandler<BrowserWindow>({
|
||||||
|
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||||
|
isDev,
|
||||||
|
getOverlayDebugVisualizationEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||||
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
|
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
|
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||||
|
isOverlayVisible: (windowKind) =>
|
||||||
|
windowKind === 'visible'
|
||||||
|
? overlayManager.getVisibleOverlayVisible()
|
||||||
|
: overlayManager.getInvisibleOverlayVisible(),
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
|
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||||
|
onWindowClosed: (windowKind) => {
|
||||||
|
if (windowKind === 'visible') {
|
||||||
|
overlayManager.setMainWindow(null);
|
||||||
|
} else {
|
||||||
|
overlayManager.setInvisibleWindow(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const createMainWindowHandler = createCreateMainWindowHandler<BrowserWindow>({
|
||||||
|
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||||
|
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||||
|
});
|
||||||
|
const createInvisibleWindowHandler = createCreateInvisibleWindowHandler<BrowserWindow>({
|
||||||
|
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||||
|
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
||||||
|
});
|
||||||
|
const resolveTrayIconPathHandler = createResolveTrayIconPathHandler({
|
||||||
|
resolveTrayIconPathRuntime,
|
||||||
|
platform: process.platform,
|
||||||
|
resourcesPath: process.resourcesPath,
|
||||||
|
appPath: app.getAppPath(),
|
||||||
|
dirname: __dirname,
|
||||||
|
joinPath: (...parts) => path.join(...parts),
|
||||||
|
fileExists: (candidate) => fs.existsSync(candidate),
|
||||||
|
});
|
||||||
|
const buildTrayMenuTemplateHandler = createBuildTrayMenuTemplateHandler({
|
||||||
|
buildTrayMenuTemplateRuntime,
|
||||||
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
|
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
|
||||||
|
openAnilistSetupWindow: () => openAnilistSetupWindow(),
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
});
|
||||||
|
const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler({
|
||||||
|
loadYomitanExtensionCore,
|
||||||
|
userDataPath: USER_DATA_PATH,
|
||||||
|
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||||
|
setYomitanParserWindow: (window) => {
|
||||||
|
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||||
|
},
|
||||||
|
setYomitanParserReadyPromise: (promise) => {
|
||||||
|
appState.yomitanParserReadyPromise = promise;
|
||||||
|
},
|
||||||
|
setYomitanParserInitPromise: (promise) => {
|
||||||
|
appState.yomitanParserInitPromise = promise;
|
||||||
|
},
|
||||||
|
setYomitanExtension: (extension) => {
|
||||||
|
appState.yomitanExt = extension;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const ensureYomitanExtensionLoadedHandler = createEnsureYomitanExtensionLoadedHandler({
|
||||||
|
getYomitanExtension: () => appState.yomitanExt,
|
||||||
|
getLoadInFlight: () => yomitanLoadInFlight,
|
||||||
|
setLoadInFlight: (promise) => {
|
||||||
|
yomitanLoadInFlight = promise;
|
||||||
|
},
|
||||||
|
loadYomitanExtension: () => loadYomitanExtension(),
|
||||||
|
});
|
||||||
|
const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler({
|
||||||
|
getBackendOverride: () => appState.backendOverride,
|
||||||
|
getInitialInvisibleOverlayVisibility: () =>
|
||||||
|
configDerivedRuntime.getInitialInvisibleOverlayVisibility(),
|
||||||
|
createMainWindow: () => {
|
||||||
|
createMainWindow();
|
||||||
|
},
|
||||||
|
createInvisibleWindow: () => {
|
||||||
|
createInvisibleWindow();
|
||||||
|
},
|
||||||
|
registerGlobalShortcuts: () => {
|
||||||
|
registerGlobalShortcuts();
|
||||||
|
},
|
||||||
|
updateVisibleOverlayBounds: (geometry) => {
|
||||||
|
updateVisibleOverlayBounds(geometry);
|
||||||
|
},
|
||||||
|
updateInvisibleOverlayBounds: (geometry) => {
|
||||||
|
updateInvisibleOverlayBounds(geometry);
|
||||||
|
},
|
||||||
|
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
|
isInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
|
updateVisibleOverlayVisibility: () => {
|
||||||
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
|
},
|
||||||
|
updateInvisibleOverlayVisibility: () => {
|
||||||
|
overlayVisibilityRuntime.updateInvisibleOverlayVisibility();
|
||||||
|
},
|
||||||
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
|
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||||
|
setWindowTracker: (tracker) => {
|
||||||
|
appState.windowTracker = tracker as never;
|
||||||
|
},
|
||||||
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||||
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
|
setAnkiIntegration: (integration) => {
|
||||||
|
appState.ankiIntegration = integration as AnkiIntegration | null;
|
||||||
|
},
|
||||||
|
showDesktopNotification,
|
||||||
|
createFieldGroupingCallback: () => createFieldGroupingCallback() as never,
|
||||||
|
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||||
|
});
|
||||||
|
|
||||||
async function updateLastCardFromClipboard(): Promise<void> {
|
async function updateLastCardFromClipboard(): Promise<void> {
|
||||||
await updateLastCardFromClipboardHandler();
|
await updateLastCardFromClipboardHandler();
|
||||||
@@ -2676,90 +2778,39 @@ function refreshOverlayShortcuts(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setVisibleOverlayVisible(visible: boolean): void {
|
function setVisibleOverlayVisible(visible: boolean): void {
|
||||||
setVisibleOverlayVisibleCore({
|
setVisibleOverlayVisibleHandler(visible);
|
||||||
visible,
|
|
||||||
setVisibleOverlayVisibleState: (nextVisible) => {
|
|
||||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
|
||||||
},
|
|
||||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
|
||||||
updateInvisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
|
||||||
syncInvisibleOverlayMousePassthrough: () =>
|
|
||||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
|
||||||
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
|
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
|
||||||
setMpvSubVisibility: (mpvSubVisible) => {
|
|
||||||
setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setInvisibleOverlayVisible(visible: boolean): void {
|
function setInvisibleOverlayVisible(visible: boolean): void {
|
||||||
setInvisibleOverlayVisibleCore({
|
setInvisibleOverlayVisibleHandler(visible);
|
||||||
visible,
|
|
||||||
setInvisibleOverlayVisibleState: (nextVisible) => {
|
|
||||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
|
||||||
},
|
|
||||||
updateInvisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
|
||||||
syncInvisibleOverlayMousePassthrough: () =>
|
|
||||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleVisibleOverlay(): void {
|
function toggleVisibleOverlay(): void {
|
||||||
setVisibleOverlayVisible(!overlayManager.getVisibleOverlayVisible());
|
toggleVisibleOverlayHandler();
|
||||||
}
|
}
|
||||||
function toggleInvisibleOverlay(): void {
|
function toggleInvisibleOverlay(): void {
|
||||||
setInvisibleOverlayVisible(!overlayManager.getInvisibleOverlayVisible());
|
toggleInvisibleOverlayHandler();
|
||||||
}
|
}
|
||||||
function setOverlayVisible(visible: boolean): void {
|
function setOverlayVisible(visible: boolean): void {
|
||||||
setVisibleOverlayVisible(visible);
|
setOverlayVisibleHandler(visible);
|
||||||
}
|
}
|
||||||
function toggleOverlay(): void {
|
function toggleOverlay(): void {
|
||||||
toggleVisibleOverlay();
|
toggleOverlayHandler();
|
||||||
}
|
}
|
||||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||||
overlayModalRuntime.handleOverlayModalClosed(modal);
|
handleOverlayModalClosedHandler(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||||
handleMpvCommandFromIpcRuntime(command, {
|
handleMpvCommandFromIpcHandler(command);
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
|
||||||
cycleRuntimeOption: (id, direction) => {
|
|
||||||
if (!appState.runtimeOptionsManager) {
|
|
||||||
return { ok: false, error: 'Runtime options manager unavailable' };
|
|
||||||
}
|
|
||||||
return applyRuntimeOptionResultRuntime(
|
|
||||||
appState.runtimeOptionsManager.cycleOption(id, direction),
|
|
||||||
(text) => showMpvOsd(text),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
|
||||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
|
||||||
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
|
||||||
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
|
||||||
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
|
||||||
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promise<SubsyncResult> {
|
async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promise<SubsyncResult> {
|
||||||
return subsyncRuntime.runManualFromIpc(request);
|
return runSubsyncManualFromIpcHandler(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendClipboardVideoToQueue(): { ok: boolean; message: string } {
|
function appendClipboardVideoToQueue(): { ok: boolean; message: string } {
|
||||||
return appendClipboardVideoToQueueRuntime({
|
return appendClipboardVideoToQueueHandler();
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
readClipboardText: () => clipboard.readText(),
|
|
||||||
showMpvOsd: (text) => showMpvOsd(text),
|
|
||||||
sendMpvCommand: (command) => {
|
|
||||||
sendMpvCommandRuntime(appState.mpvClient, command);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerIpcRuntimeServices({
|
registerIpcRuntimeServices({
|
||||||
|
|||||||
83
src/main/runtime/cli-command-context-deps.test.ts
Normal file
83
src/main/runtime/cli-command-context-deps.test.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
||||||
|
|
||||||
|
test('build cli command context deps maps handlers and values', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const buildDeps = createBuildCliCommandContextDepsHandler({
|
||||||
|
getSocketPath: () => '/tmp/mpv.sock',
|
||||||
|
setSocketPath: (socketPath) => calls.push(`socket:${socketPath}`),
|
||||||
|
getMpvClient: () => null,
|
||||||
|
showOsd: (text) => calls.push(`osd:${text}`),
|
||||||
|
texthookerService: { start: () => null, status: () => ({ running: false }) } as never,
|
||||||
|
getTexthookerPort: () => 5174,
|
||||||
|
setTexthookerPort: (port) => calls.push(`port:${port}`),
|
||||||
|
shouldOpenBrowser: () => true,
|
||||||
|
openExternal: async (url) => calls.push(`open:${url}`),
|
||||||
|
logBrowserOpenError: (url) => calls.push(`open-error:${url}`),
|
||||||
|
isOverlayInitialized: () => true,
|
||||||
|
initializeOverlay: () => calls.push('init'),
|
||||||
|
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||||
|
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||||
|
setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`),
|
||||||
|
setInvisibleOverlay: (visible) => calls.push(`set-invisible:${visible}`),
|
||||||
|
copyCurrentSubtitle: () => calls.push('copy'),
|
||||||
|
startPendingMultiCopy: (ms) => calls.push(`multi:${ms}`),
|
||||||
|
mineSentenceCard: async () => {
|
||||||
|
calls.push('mine');
|
||||||
|
},
|
||||||
|
startPendingMineSentenceMultiple: (ms) => calls.push(`mine-multi:${ms}`),
|
||||||
|
updateLastCardFromClipboard: async () => {
|
||||||
|
calls.push('update');
|
||||||
|
},
|
||||||
|
refreshKnownWordCache: async () => {
|
||||||
|
calls.push('refresh');
|
||||||
|
},
|
||||||
|
triggerFieldGrouping: async () => {
|
||||||
|
calls.push('group');
|
||||||
|
},
|
||||||
|
triggerSubsyncFromConfig: async () => {
|
||||||
|
calls.push('subsync');
|
||||||
|
},
|
||||||
|
markLastCardAsAudioCard: async () => {
|
||||||
|
calls.push('mark');
|
||||||
|
},
|
||||||
|
getAnilistStatus: () => ({}) as never,
|
||||||
|
clearAnilistToken: () => calls.push('clear-token'),
|
||||||
|
openAnilistSetup: () => calls.push('anilist'),
|
||||||
|
openJellyfinSetup: () => calls.push('jellyfin'),
|
||||||
|
getAnilistQueueStatus: () => ({}) as never,
|
||||||
|
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||||
|
runJellyfinCommand: async () => {
|
||||||
|
calls.push('run-jellyfin');
|
||||||
|
},
|
||||||
|
openYomitanSettings: () => calls.push('yomitan'),
|
||||||
|
cycleSecondarySubMode: () => calls.push('cycle-secondary'),
|
||||||
|
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||||
|
printHelp: () => calls.push('help'),
|
||||||
|
stopApp: () => calls.push('stop'),
|
||||||
|
hasMainWindow: () => true,
|
||||||
|
getMultiCopyTimeoutMs: () => 5000,
|
||||||
|
schedule: (fn) => {
|
||||||
|
fn();
|
||||||
|
return setTimeout(() => {}, 0);
|
||||||
|
},
|
||||||
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
|
logWarn: (message) => calls.push(`warn:${message}`),
|
||||||
|
logError: (message) => calls.push(`error:${message}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deps = buildDeps();
|
||||||
|
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
|
||||||
|
assert.equal(deps.getTexthookerPort(), 5174);
|
||||||
|
assert.equal(deps.shouldOpenBrowser(), true);
|
||||||
|
assert.equal(deps.isOverlayInitialized(), true);
|
||||||
|
assert.equal(deps.hasMainWindow(), true);
|
||||||
|
assert.equal(deps.getMultiCopyTimeoutMs(), 5000);
|
||||||
|
|
||||||
|
deps.setSocketPath('/tmp/next.sock');
|
||||||
|
deps.showOsd('hello');
|
||||||
|
deps.setTexthookerPort(5175);
|
||||||
|
deps.printHelp();
|
||||||
|
assert.deepEqual(calls, ['socket:/tmp/next.sock', 'osd:hello', 'port:5175', 'help']);
|
||||||
|
});
|
||||||
94
src/main/runtime/cli-command-context-deps.ts
Normal file
94
src/main/runtime/cli-command-context-deps.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import type { CliArgs } from '../../cli/args';
|
||||||
|
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||||
|
|
||||||
|
export function createBuildCliCommandContextDepsHandler(deps: {
|
||||||
|
getSocketPath: () => string;
|
||||||
|
setSocketPath: (socketPath: string) => void;
|
||||||
|
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
||||||
|
showOsd: (text: string) => void;
|
||||||
|
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||||
|
getTexthookerPort: () => number;
|
||||||
|
setTexthookerPort: (port: number) => void;
|
||||||
|
shouldOpenBrowser: () => boolean;
|
||||||
|
openExternal: (url: string) => Promise<unknown>;
|
||||||
|
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||||
|
isOverlayInitialized: () => boolean;
|
||||||
|
initializeOverlay: () => void;
|
||||||
|
toggleVisibleOverlay: () => void;
|
||||||
|
toggleInvisibleOverlay: () => void;
|
||||||
|
setVisibleOverlay: (visible: boolean) => void;
|
||||||
|
setInvisibleOverlay: (visible: boolean) => void;
|
||||||
|
copyCurrentSubtitle: () => void;
|
||||||
|
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||||
|
mineSentenceCard: () => Promise<void>;
|
||||||
|
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||||
|
updateLastCardFromClipboard: () => Promise<void>;
|
||||||
|
refreshKnownWordCache: () => Promise<void>;
|
||||||
|
triggerFieldGrouping: () => Promise<void>;
|
||||||
|
triggerSubsyncFromConfig: () => Promise<void>;
|
||||||
|
markLastCardAsAudioCard: () => Promise<void>;
|
||||||
|
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||||
|
clearAnilistToken: CliCommandContextFactoryDeps['clearAnilistToken'];
|
||||||
|
openAnilistSetup: CliCommandContextFactoryDeps['openAnilistSetup'];
|
||||||
|
openJellyfinSetup: CliCommandContextFactoryDeps['openJellyfinSetup'];
|
||||||
|
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||||
|
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||||
|
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||||
|
openYomitanSettings: () => void;
|
||||||
|
cycleSecondarySubMode: () => void;
|
||||||
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
printHelp: () => void;
|
||||||
|
stopApp: () => void;
|
||||||
|
hasMainWindow: () => boolean;
|
||||||
|
getMultiCopyTimeoutMs: () => number;
|
||||||
|
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
logWarn: (message: string) => void;
|
||||||
|
logError: (message: string, err: unknown) => void;
|
||||||
|
}) {
|
||||||
|
return (): CliCommandContextFactoryDeps => ({
|
||||||
|
getSocketPath: deps.getSocketPath,
|
||||||
|
setSocketPath: deps.setSocketPath,
|
||||||
|
getMpvClient: deps.getMpvClient,
|
||||||
|
showOsd: deps.showOsd,
|
||||||
|
texthookerService: deps.texthookerService,
|
||||||
|
getTexthookerPort: deps.getTexthookerPort,
|
||||||
|
setTexthookerPort: deps.setTexthookerPort,
|
||||||
|
shouldOpenBrowser: deps.shouldOpenBrowser,
|
||||||
|
openExternal: deps.openExternal,
|
||||||
|
logBrowserOpenError: deps.logBrowserOpenError,
|
||||||
|
isOverlayInitialized: deps.isOverlayInitialized,
|
||||||
|
initializeOverlay: deps.initializeOverlay,
|
||||||
|
toggleVisibleOverlay: deps.toggleVisibleOverlay,
|
||||||
|
toggleInvisibleOverlay: deps.toggleInvisibleOverlay,
|
||||||
|
setVisibleOverlay: deps.setVisibleOverlay,
|
||||||
|
setInvisibleOverlay: deps.setInvisibleOverlay,
|
||||||
|
copyCurrentSubtitle: deps.copyCurrentSubtitle,
|
||||||
|
startPendingMultiCopy: deps.startPendingMultiCopy,
|
||||||
|
mineSentenceCard: deps.mineSentenceCard,
|
||||||
|
startPendingMineSentenceMultiple: deps.startPendingMineSentenceMultiple,
|
||||||
|
updateLastCardFromClipboard: deps.updateLastCardFromClipboard,
|
||||||
|
refreshKnownWordCache: deps.refreshKnownWordCache,
|
||||||
|
triggerFieldGrouping: deps.triggerFieldGrouping,
|
||||||
|
triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig,
|
||||||
|
markLastCardAsAudioCard: deps.markLastCardAsAudioCard,
|
||||||
|
getAnilistStatus: deps.getAnilistStatus,
|
||||||
|
clearAnilistToken: deps.clearAnilistToken,
|
||||||
|
openAnilistSetup: deps.openAnilistSetup,
|
||||||
|
openJellyfinSetup: deps.openJellyfinSetup,
|
||||||
|
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
||||||
|
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
||||||
|
runJellyfinCommand: deps.runJellyfinCommand,
|
||||||
|
openYomitanSettings: deps.openYomitanSettings,
|
||||||
|
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
||||||
|
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,
|
||||||
|
printHelp: deps.printHelp,
|
||||||
|
stopApp: deps.stopApp,
|
||||||
|
hasMainWindow: deps.hasMainWindow,
|
||||||
|
getMultiCopyTimeoutMs: deps.getMultiCopyTimeoutMs,
|
||||||
|
schedule: deps.schedule,
|
||||||
|
logInfo: deps.logInfo,
|
||||||
|
logWarn: deps.logWarn,
|
||||||
|
logError: deps.logError,
|
||||||
|
});
|
||||||
|
}
|
||||||
45
src/main/runtime/ipc-bridge-actions.test.ts
Normal file
45
src/main/runtime/ipc-bridge-actions.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createHandleMpvCommandFromIpcHandler,
|
||||||
|
createRunSubsyncManualFromIpcHandler,
|
||||||
|
} from './ipc-bridge-actions';
|
||||||
|
|
||||||
|
test('handle mpv command handler forwards command and built deps', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = {
|
||||||
|
triggerSubsyncFromConfig: () => {},
|
||||||
|
openRuntimeOptionsPalette: () => {},
|
||||||
|
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
||||||
|
showMpvOsd: () => {},
|
||||||
|
replayCurrentSubtitle: () => {},
|
||||||
|
playNextSubtitle: () => {},
|
||||||
|
sendMpvCommand: () => {},
|
||||||
|
isMpvConnected: () => true,
|
||||||
|
hasRuntimeOptionsManager: () => true,
|
||||||
|
};
|
||||||
|
const handle = createHandleMpvCommandFromIpcHandler({
|
||||||
|
handleMpvCommandFromIpcRuntime: (command, nextDeps) => {
|
||||||
|
calls.push(`command:${command.join(':')}`);
|
||||||
|
assert.equal(nextDeps, deps);
|
||||||
|
},
|
||||||
|
buildMpvCommandDeps: () => deps,
|
||||||
|
});
|
||||||
|
|
||||||
|
handle(['show-text', 'hello']);
|
||||||
|
assert.deepEqual(calls, ['command:show-text:hello']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('run subsync manual handler forwards request and result', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const run = createRunSubsyncManualFromIpcHandler({
|
||||||
|
runManualFromIpc: async (request: { id: string }) => {
|
||||||
|
calls.push(`request:${request.id}`);
|
||||||
|
return { ok: true as const };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await run({ id: 'job-1' });
|
||||||
|
assert.deepEqual(result, { ok: true });
|
||||||
|
assert.deepEqual(calls, ['request:job-1']);
|
||||||
|
});
|
||||||
21
src/main/runtime/ipc-bridge-actions.ts
Normal file
21
src/main/runtime/ipc-bridge-actions.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
|
||||||
|
|
||||||
|
export function createHandleMpvCommandFromIpcHandler(deps: {
|
||||||
|
handleMpvCommandFromIpcRuntime: (
|
||||||
|
command: (string | number)[],
|
||||||
|
options: MpvCommandFromIpcRuntimeDeps,
|
||||||
|
) => void;
|
||||||
|
buildMpvCommandDeps: () => MpvCommandFromIpcRuntimeDeps;
|
||||||
|
}) {
|
||||||
|
return (command: (string | number)[]): void => {
|
||||||
|
deps.handleMpvCommandFromIpcRuntime(command, deps.buildMpvCommandDeps());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRunSubsyncManualFromIpcHandler<TRequest, TResult>(deps: {
|
||||||
|
runManualFromIpc: (request: TRequest) => Promise<TResult>;
|
||||||
|
}) {
|
||||||
|
return async (request: TRequest): Promise<TResult> => {
|
||||||
|
return deps.runManualFromIpc(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
60
src/main/runtime/overlay-main-actions.test.ts
Normal file
60
src/main/runtime/overlay-main-actions.test.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createAppendClipboardVideoToQueueHandler,
|
||||||
|
createHandleOverlayModalClosedHandler,
|
||||||
|
createSetOverlayVisibleHandler,
|
||||||
|
createToggleOverlayHandler,
|
||||||
|
} from './overlay-main-actions';
|
||||||
|
|
||||||
|
test('set overlay visible handler delegates to visible overlay setter', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const setOverlayVisible = createSetOverlayVisibleHandler({
|
||||||
|
setVisibleOverlayVisible: (visible) => calls.push(`set:${visible}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
setOverlayVisible(true);
|
||||||
|
assert.deepEqual(calls, ['set:true']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggle overlay handler delegates to visible toggle', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const toggleOverlay = createToggleOverlayHandler({
|
||||||
|
toggleVisibleOverlay: () => calls.push('toggle'),
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleOverlay();
|
||||||
|
assert.deepEqual(calls, ['toggle']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlay modal closed handler delegates to runtime handler', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const handleClosed = createHandleOverlayModalClosedHandler({
|
||||||
|
handleOverlayModalClosedRuntime: (modal) => calls.push(`closed:${modal}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
handleClosed('runtime-options');
|
||||||
|
assert.deepEqual(calls, ['closed:runtime-options']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('append clipboard queue handler forwards runtime deps and result', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const mpvClient = { connected: true };
|
||||||
|
const appendClipboardVideoToQueue = createAppendClipboardVideoToQueueHandler({
|
||||||
|
appendClipboardVideoToQueueRuntime: (options) => {
|
||||||
|
assert.equal(options.getMpvClient(), mpvClient);
|
||||||
|
assert.equal(options.readClipboardText(), '/tmp/video.mkv');
|
||||||
|
options.showMpvOsd('queued');
|
||||||
|
options.sendMpvCommand(['loadfile', '/tmp/video.mkv', 'append']);
|
||||||
|
return { ok: true, message: 'ok' };
|
||||||
|
},
|
||||||
|
getMpvClient: () => mpvClient,
|
||||||
|
readClipboardText: () => '/tmp/video.mkv',
|
||||||
|
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||||
|
sendMpvCommand: (command) => calls.push(`mpv:${command.join(':')}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = appendClipboardVideoToQueue();
|
||||||
|
assert.deepEqual(result, { ok: true, message: 'ok' });
|
||||||
|
assert.deepEqual(calls, ['osd:queued', 'mpv:loadfile:/tmp/video.mkv:append']);
|
||||||
|
});
|
||||||
47
src/main/runtime/overlay-main-actions.ts
Normal file
47
src/main/runtime/overlay-main-actions.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { OverlayHostedModal } from '../overlay-runtime';
|
||||||
|
import type { AppendClipboardVideoToQueueRuntimeDeps } from './clipboard-queue';
|
||||||
|
|
||||||
|
export function createSetOverlayVisibleHandler(deps: {
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (visible: boolean): void => {
|
||||||
|
deps.setVisibleOverlayVisible(visible);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createToggleOverlayHandler(deps: {
|
||||||
|
toggleVisibleOverlay: () => void;
|
||||||
|
}) {
|
||||||
|
return (): void => {
|
||||||
|
deps.toggleVisibleOverlay();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHandleOverlayModalClosedHandler(deps: {
|
||||||
|
handleOverlayModalClosedRuntime: (modal: OverlayHostedModal) => void;
|
||||||
|
}) {
|
||||||
|
return (modal: OverlayHostedModal): void => {
|
||||||
|
deps.handleOverlayModalClosedRuntime(modal);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAppendClipboardVideoToQueueHandler(deps: {
|
||||||
|
appendClipboardVideoToQueueRuntime: (
|
||||||
|
options: AppendClipboardVideoToQueueRuntimeDeps,
|
||||||
|
) => { ok: boolean; message: string };
|
||||||
|
getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T
|
||||||
|
? T
|
||||||
|
: never;
|
||||||
|
readClipboardText: () => string;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
sendMpvCommand: (command: (string | number)[]) => void;
|
||||||
|
}) {
|
||||||
|
return (): { ok: boolean; message: string } => {
|
||||||
|
return deps.appendClipboardVideoToQueueRuntime({
|
||||||
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
readClipboardText: deps.readClipboardText,
|
||||||
|
showMpvOsd: deps.showMpvOsd,
|
||||||
|
sendMpvCommand: deps.sendMpvCommand,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
70
src/main/runtime/overlay-runtime-options.test.ts
Normal file
70
src/main/runtime/overlay-runtime-options.test.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||||
|
|
||||||
|
test('build initialize overlay runtime options maps dependencies', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const buildOptions = createBuildInitializeOverlayRuntimeOptionsHandler({
|
||||||
|
getBackendOverride: () => 'x11',
|
||||||
|
getInitialInvisibleOverlayVisibility: () => true,
|
||||||
|
createMainWindow: () => calls.push('create-main'),
|
||||||
|
createInvisibleWindow: () => calls.push('create-invisible'),
|
||||||
|
registerGlobalShortcuts: () => calls.push('register-shortcuts'),
|
||||||
|
updateVisibleOverlayBounds: () => calls.push('update-visible-bounds'),
|
||||||
|
updateInvisibleOverlayBounds: () => calls.push('update-invisible-bounds'),
|
||||||
|
isVisibleOverlayVisible: () => true,
|
||||||
|
isInvisibleOverlayVisible: () => false,
|
||||||
|
updateVisibleOverlayVisibility: () => calls.push('update-visible'),
|
||||||
|
updateInvisibleOverlayVisibility: () => calls.push('update-invisible'),
|
||||||
|
getOverlayWindows: () => [],
|
||||||
|
syncOverlayShortcuts: () => calls.push('sync-shortcuts'),
|
||||||
|
setWindowTracker: () => calls.push('set-tracker'),
|
||||||
|
getResolvedConfig: () => ({}),
|
||||||
|
getSubtitleTimingTracker: () => null,
|
||||||
|
getMpvClient: () => null,
|
||||||
|
getMpvSocketPath: () => '/tmp/mpv.sock',
|
||||||
|
getRuntimeOptionsManager: () => null,
|
||||||
|
setAnkiIntegration: () => calls.push('set-anki'),
|
||||||
|
showDesktopNotification: () => calls.push('notify'),
|
||||||
|
createFieldGroupingCallback: () => async () => ({
|
||||||
|
keepNoteId: 1,
|
||||||
|
deleteNoteId: 2,
|
||||||
|
deleteDuplicate: false,
|
||||||
|
cancelled: false,
|
||||||
|
}),
|
||||||
|
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = buildOptions();
|
||||||
|
assert.equal(options.backendOverride, 'x11');
|
||||||
|
assert.equal(options.getInitialInvisibleOverlayVisibility(), true);
|
||||||
|
assert.equal(options.isVisibleOverlayVisible(), true);
|
||||||
|
assert.equal(options.isInvisibleOverlayVisible(), false);
|
||||||
|
assert.equal(options.getMpvSocketPath(), '/tmp/mpv.sock');
|
||||||
|
assert.equal(options.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json');
|
||||||
|
options.createMainWindow();
|
||||||
|
options.createInvisibleWindow();
|
||||||
|
options.registerGlobalShortcuts();
|
||||||
|
options.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||||
|
options.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||||
|
options.updateVisibleOverlayVisibility();
|
||||||
|
options.updateInvisibleOverlayVisibility();
|
||||||
|
options.syncOverlayShortcuts();
|
||||||
|
options.setWindowTracker(null);
|
||||||
|
options.setAnkiIntegration(null);
|
||||||
|
options.showDesktopNotification('title', {});
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'create-main',
|
||||||
|
'create-invisible',
|
||||||
|
'register-shortcuts',
|
||||||
|
'update-visible-bounds',
|
||||||
|
'update-invisible-bounds',
|
||||||
|
'update-visible',
|
||||||
|
'update-invisible',
|
||||||
|
'sync-shortcuts',
|
||||||
|
'set-tracker',
|
||||||
|
'set-anki',
|
||||||
|
'notify',
|
||||||
|
]);
|
||||||
|
});
|
||||||
93
src/main/runtime/overlay-runtime-options.ts
Normal file
93
src/main/runtime/overlay-runtime-options.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import type {
|
||||||
|
AnkiConnectConfig,
|
||||||
|
KikuFieldGroupingChoice,
|
||||||
|
KikuFieldGroupingRequestData,
|
||||||
|
WindowGeometry,
|
||||||
|
} from '../../types';
|
||||||
|
import type { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
type OverlayRuntimeOptions = {
|
||||||
|
backendOverride: string | null;
|
||||||
|
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||||
|
createMainWindow: () => void;
|
||||||
|
createInvisibleWindow: () => void;
|
||||||
|
registerGlobalShortcuts: () => void;
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
isVisibleOverlayVisible: () => boolean;
|
||||||
|
isInvisibleOverlayVisible: () => boolean;
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
updateInvisibleOverlayVisibility: () => void;
|
||||||
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
setWindowTracker: (tracker: unknown | null) => void;
|
||||||
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
|
getSubtitleTimingTracker: () => unknown | null;
|
||||||
|
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
|
getRuntimeOptionsManager: () => {
|
||||||
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||||
|
} | null;
|
||||||
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
createFieldGroupingCallback: () => (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
getKnownWordCacheStatePath: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||||
|
getBackendOverride: () => string | null;
|
||||||
|
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||||
|
createMainWindow: () => void;
|
||||||
|
createInvisibleWindow: () => void;
|
||||||
|
registerGlobalShortcuts: () => void;
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
isVisibleOverlayVisible: () => boolean;
|
||||||
|
isInvisibleOverlayVisible: () => boolean;
|
||||||
|
updateVisibleOverlayVisibility: () => void;
|
||||||
|
updateInvisibleOverlayVisibility: () => void;
|
||||||
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
setWindowTracker: (tracker: unknown | null) => void;
|
||||||
|
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||||
|
getSubtitleTimingTracker: () => unknown | null;
|
||||||
|
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||||
|
getMpvSocketPath: () => string;
|
||||||
|
getRuntimeOptionsManager: () => {
|
||||||
|
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||||
|
} | null;
|
||||||
|
setAnkiIntegration: (integration: unknown | null) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||||
|
createFieldGroupingCallback: () => (
|
||||||
|
data: KikuFieldGroupingRequestData,
|
||||||
|
) => Promise<KikuFieldGroupingChoice>;
|
||||||
|
getKnownWordCacheStatePath: () => string;
|
||||||
|
}) {
|
||||||
|
return (): OverlayRuntimeOptions => ({
|
||||||
|
backendOverride: deps.getBackendOverride(),
|
||||||
|
getInitialInvisibleOverlayVisibility: deps.getInitialInvisibleOverlayVisibility,
|
||||||
|
createMainWindow: deps.createMainWindow,
|
||||||
|
createInvisibleWindow: deps.createInvisibleWindow,
|
||||||
|
registerGlobalShortcuts: deps.registerGlobalShortcuts,
|
||||||
|
updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds,
|
||||||
|
updateInvisibleOverlayBounds: deps.updateInvisibleOverlayBounds,
|
||||||
|
isVisibleOverlayVisible: deps.isVisibleOverlayVisible,
|
||||||
|
isInvisibleOverlayVisible: deps.isInvisibleOverlayVisible,
|
||||||
|
updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility,
|
||||||
|
updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility,
|
||||||
|
getOverlayWindows: deps.getOverlayWindows,
|
||||||
|
syncOverlayShortcuts: deps.syncOverlayShortcuts,
|
||||||
|
setWindowTracker: deps.setWindowTracker,
|
||||||
|
getResolvedConfig: deps.getResolvedConfig,
|
||||||
|
getSubtitleTimingTracker: deps.getSubtitleTimingTracker,
|
||||||
|
getMpvClient: deps.getMpvClient,
|
||||||
|
getMpvSocketPath: deps.getMpvSocketPath,
|
||||||
|
getRuntimeOptionsManager: deps.getRuntimeOptionsManager,
|
||||||
|
setAnkiIntegration: deps.setAnkiIntegration,
|
||||||
|
showDesktopNotification: deps.showDesktopNotification,
|
||||||
|
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
||||||
|
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
66
src/main/runtime/overlay-window-factory.test.ts
Normal file
66
src/main/runtime/overlay-window-factory.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createCreateInvisibleWindowHandler,
|
||||||
|
createCreateMainWindowHandler,
|
||||||
|
createCreateOverlayWindowHandler,
|
||||||
|
} from './overlay-window-factory';
|
||||||
|
|
||||||
|
test('create overlay window handler forwards options and kind', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const window = { id: 1 };
|
||||||
|
const createOverlayWindow = createCreateOverlayWindowHandler({
|
||||||
|
createOverlayWindowCore: (kind, options) => {
|
||||||
|
calls.push(`kind:${kind}`);
|
||||||
|
assert.equal(options.isDev, true);
|
||||||
|
assert.equal(options.overlayDebugVisualizationEnabled, false);
|
||||||
|
assert.equal(options.isOverlayVisible('visible'), true);
|
||||||
|
assert.equal(options.isOverlayVisible('invisible'), false);
|
||||||
|
options.onRuntimeOptionsChanged();
|
||||||
|
options.setOverlayDebugVisualizationEnabled(true);
|
||||||
|
options.onWindowClosed(kind);
|
||||||
|
return window;
|
||||||
|
},
|
||||||
|
isDev: true,
|
||||||
|
getOverlayDebugVisualizationEnabled: () => false,
|
||||||
|
ensureOverlayWindowLevel: () => {},
|
||||||
|
onRuntimeOptionsChanged: () => calls.push('runtime-options'),
|
||||||
|
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
||||||
|
isOverlayVisible: (kind) => kind === 'visible',
|
||||||
|
tryHandleOverlayShortcutLocalFallback: () => false,
|
||||||
|
onWindowClosed: (kind) => calls.push(`closed:${kind}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(createOverlayWindow('visible'), window);
|
||||||
|
assert.deepEqual(calls, ['kind:visible', 'runtime-options', 'debug:true', 'closed:visible']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create main window handler stores visible window', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const visibleWindow = { id: 'visible' };
|
||||||
|
const createMainWindow = createCreateMainWindowHandler({
|
||||||
|
createOverlayWindow: (kind) => {
|
||||||
|
calls.push(`create:${kind}`);
|
||||||
|
return visibleWindow;
|
||||||
|
},
|
||||||
|
setMainWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(createMainWindow(), visibleWindow);
|
||||||
|
assert.deepEqual(calls, ['create:visible', 'set:visible']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create invisible window handler stores invisible window', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const invisibleWindow = { id: 'invisible' };
|
||||||
|
const createInvisibleWindow = createCreateInvisibleWindowHandler({
|
||||||
|
createOverlayWindow: (kind) => {
|
||||||
|
calls.push(`create:${kind}`);
|
||||||
|
return invisibleWindow;
|
||||||
|
},
|
||||||
|
setInvisibleWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(createInvisibleWindow(), invisibleWindow);
|
||||||
|
assert.deepEqual(calls, ['create:invisible', 'set:invisible']);
|
||||||
|
});
|
||||||
60
src/main/runtime/overlay-window-factory.ts
Normal file
60
src/main/runtime/overlay-window-factory.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
type OverlayWindowKind = 'visible' | 'invisible';
|
||||||
|
|
||||||
|
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||||
|
createOverlayWindowCore: (
|
||||||
|
kind: OverlayWindowKind,
|
||||||
|
options: {
|
||||||
|
isDev: boolean;
|
||||||
|
overlayDebugVisualizationEnabled: boolean;
|
||||||
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
|
onRuntimeOptionsChanged: () => void;
|
||||||
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
|
isOverlayVisible: (windowKind: OverlayWindowKind) => boolean;
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
|
onWindowClosed: (windowKind: OverlayWindowKind) => void;
|
||||||
|
},
|
||||||
|
) => TWindow;
|
||||||
|
isDev: boolean;
|
||||||
|
getOverlayDebugVisualizationEnabled: () => boolean;
|
||||||
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
|
onRuntimeOptionsChanged: () => void;
|
||||||
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
|
isOverlayVisible: (windowKind: OverlayWindowKind) => boolean;
|
||||||
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
|
onWindowClosed: (windowKind: OverlayWindowKind) => void;
|
||||||
|
}) {
|
||||||
|
return (kind: OverlayWindowKind): TWindow => {
|
||||||
|
return deps.createOverlayWindowCore(kind, {
|
||||||
|
isDev: deps.isDev,
|
||||||
|
overlayDebugVisualizationEnabled: deps.getOverlayDebugVisualizationEnabled(),
|
||||||
|
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
||||||
|
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
||||||
|
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
||||||
|
isOverlayVisible: deps.isOverlayVisible,
|
||||||
|
tryHandleOverlayShortcutLocalFallback: deps.tryHandleOverlayShortcutLocalFallback,
|
||||||
|
onWindowClosed: deps.onWindowClosed,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCreateMainWindowHandler<TWindow>(deps: {
|
||||||
|
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||||
|
setMainWindow: (window: TWindow | null) => void;
|
||||||
|
}) {
|
||||||
|
return (): TWindow => {
|
||||||
|
const window = deps.createOverlayWindow('visible');
|
||||||
|
deps.setMainWindow(window);
|
||||||
|
return window;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCreateInvisibleWindowHandler<TWindow>(deps: {
|
||||||
|
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||||
|
setInvisibleWindow: (window: TWindow | null) => void;
|
||||||
|
}) {
|
||||||
|
return (): TWindow => {
|
||||||
|
const window = deps.createOverlayWindow('invisible');
|
||||||
|
deps.setInvisibleWindow(window);
|
||||||
|
return window;
|
||||||
|
};
|
||||||
|
}
|
||||||
76
src/main/runtime/tray-main-actions.test.ts
Normal file
76
src/main/runtime/tray-main-actions.test.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createBuildTrayMenuTemplateHandler,
|
||||||
|
createResolveTrayIconPathHandler,
|
||||||
|
} from './tray-main-actions';
|
||||||
|
|
||||||
|
test('resolve tray icon path handler forwards runtime dependencies', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const resolveTrayIconPath = createResolveTrayIconPathHandler({
|
||||||
|
resolveTrayIconPathRuntime: (options) => {
|
||||||
|
calls.push(`platform:${options.platform}`);
|
||||||
|
calls.push(`resources:${options.resourcesPath}`);
|
||||||
|
calls.push(`app:${options.appPath}`);
|
||||||
|
calls.push(`dir:${options.dirname}`);
|
||||||
|
calls.push(`join:${options.joinPath('a', 'b')}`);
|
||||||
|
calls.push(`exists:${options.fileExists('/tmp/icon.png')}`);
|
||||||
|
return '/tmp/icon.png';
|
||||||
|
},
|
||||||
|
platform: 'darwin',
|
||||||
|
resourcesPath: '/resources',
|
||||||
|
appPath: '/app',
|
||||||
|
dirname: '/dir',
|
||||||
|
joinPath: (...parts) => parts.join('/'),
|
||||||
|
fileExists: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(resolveTrayIconPath(), '/tmp/icon.png');
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'platform:darwin',
|
||||||
|
'resources:/resources',
|
||||||
|
'app:/app',
|
||||||
|
'dir:/dir',
|
||||||
|
'join:a/b',
|
||||||
|
'exists:true',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('build tray template handler wires actions and init guards', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let initialized = false;
|
||||||
|
const buildTemplate = createBuildTrayMenuTemplateHandler({
|
||||||
|
buildTrayMenuTemplateRuntime: (handlers) => {
|
||||||
|
handlers.openOverlay();
|
||||||
|
handlers.openYomitanSettings();
|
||||||
|
handlers.openRuntimeOptions();
|
||||||
|
handlers.openJellyfinSetup();
|
||||||
|
handlers.openAnilistSetup();
|
||||||
|
handlers.quitApp();
|
||||||
|
return [{ label: 'ok' }] as never;
|
||||||
|
},
|
||||||
|
initializeOverlayRuntime: () => {
|
||||||
|
initialized = true;
|
||||||
|
calls.push('init');
|
||||||
|
},
|
||||||
|
isOverlayRuntimeInitialized: () => initialized,
|
||||||
|
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||||
|
openYomitanSettings: () => calls.push('yomitan'),
|
||||||
|
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||||
|
openJellyfinSetupWindow: () => calls.push('jellyfin'),
|
||||||
|
openAnilistSetupWindow: () => calls.push('anilist'),
|
||||||
|
quitApp: () => calls.push('quit'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = buildTemplate();
|
||||||
|
assert.deepEqual(template, [{ label: 'ok' }]);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'init',
|
||||||
|
'visible:true',
|
||||||
|
'yomitan',
|
||||||
|
'runtime-options',
|
||||||
|
'jellyfin',
|
||||||
|
'anilist',
|
||||||
|
'quit',
|
||||||
|
]);
|
||||||
|
});
|
||||||
75
src/main/runtime/tray-main-actions.ts
Normal file
75
src/main/runtime/tray-main-actions.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export function createResolveTrayIconPathHandler(deps: {
|
||||||
|
resolveTrayIconPathRuntime: (options: {
|
||||||
|
platform: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
appPath: string;
|
||||||
|
dirname: string;
|
||||||
|
joinPath: (...parts: string[]) => string;
|
||||||
|
fileExists: (path: string) => boolean;
|
||||||
|
}) => string | null;
|
||||||
|
platform: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
appPath: string;
|
||||||
|
dirname: string;
|
||||||
|
joinPath: (...parts: string[]) => string;
|
||||||
|
fileExists: (path: string) => boolean;
|
||||||
|
}) {
|
||||||
|
return (): string | null => {
|
||||||
|
return deps.resolveTrayIconPathRuntime({
|
||||||
|
platform: deps.platform,
|
||||||
|
resourcesPath: deps.resourcesPath,
|
||||||
|
appPath: deps.appPath,
|
||||||
|
dirname: deps.dirname,
|
||||||
|
joinPath: deps.joinPath,
|
||||||
|
fileExists: deps.fileExists,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
||||||
|
buildTrayMenuTemplateRuntime: (handlers: {
|
||||||
|
openOverlay: () => void;
|
||||||
|
openYomitanSettings: () => void;
|
||||||
|
openRuntimeOptions: () => void;
|
||||||
|
openJellyfinSetup: () => void;
|
||||||
|
openAnilistSetup: () => void;
|
||||||
|
quitApp: () => void;
|
||||||
|
}) => TMenuItem[];
|
||||||
|
initializeOverlayRuntime: () => void;
|
||||||
|
isOverlayRuntimeInitialized: () => boolean;
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
openYomitanSettings: () => void;
|
||||||
|
openRuntimeOptionsPalette: () => void;
|
||||||
|
openJellyfinSetupWindow: () => void;
|
||||||
|
openAnilistSetupWindow: () => void;
|
||||||
|
quitApp: () => void;
|
||||||
|
}) {
|
||||||
|
return (): TMenuItem[] => {
|
||||||
|
return deps.buildTrayMenuTemplateRuntime({
|
||||||
|
openOverlay: () => {
|
||||||
|
if (!deps.isOverlayRuntimeInitialized()) {
|
||||||
|
deps.initializeOverlayRuntime();
|
||||||
|
}
|
||||||
|
deps.setVisibleOverlayVisible(true);
|
||||||
|
},
|
||||||
|
openYomitanSettings: () => {
|
||||||
|
deps.openYomitanSettings();
|
||||||
|
},
|
||||||
|
openRuntimeOptions: () => {
|
||||||
|
if (!deps.isOverlayRuntimeInitialized()) {
|
||||||
|
deps.initializeOverlayRuntime();
|
||||||
|
}
|
||||||
|
deps.openRuntimeOptionsPalette();
|
||||||
|
},
|
||||||
|
openJellyfinSetup: () => {
|
||||||
|
deps.openJellyfinSetupWindow();
|
||||||
|
},
|
||||||
|
openAnilistSetup: () => {
|
||||||
|
deps.openAnilistSetupWindow();
|
||||||
|
},
|
||||||
|
quitApp: () => {
|
||||||
|
deps.quitApp();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
86
src/main/runtime/yomitan-extension-loader.test.ts
Normal file
86
src/main/runtime/yomitan-extension-loader.test.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
createEnsureYomitanExtensionLoadedHandler,
|
||||||
|
createLoadYomitanExtensionHandler,
|
||||||
|
} from './yomitan-extension-loader';
|
||||||
|
|
||||||
|
test('load yomitan extension handler forwards parser state dependencies', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const parserWindow = {} as never;
|
||||||
|
const extension = { id: 'ext' } as never;
|
||||||
|
const loadYomitanExtension = createLoadYomitanExtensionHandler({
|
||||||
|
loadYomitanExtensionCore: async (options) => {
|
||||||
|
calls.push(`path:${options.userDataPath}`);
|
||||||
|
assert.equal(options.getYomitanParserWindow(), parserWindow);
|
||||||
|
options.setYomitanParserWindow(null);
|
||||||
|
options.setYomitanParserReadyPromise(null);
|
||||||
|
options.setYomitanParserInitPromise(null);
|
||||||
|
options.setYomitanExtension(extension);
|
||||||
|
return extension;
|
||||||
|
},
|
||||||
|
userDataPath: '/tmp/subminer',
|
||||||
|
getYomitanParserWindow: () => parserWindow,
|
||||||
|
setYomitanParserWindow: () => calls.push('set-window'),
|
||||||
|
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||||
|
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||||
|
setYomitanExtension: () => calls.push('set-ext'),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await loadYomitanExtension(), extension);
|
||||||
|
assert.deepEqual(calls, ['path:/tmp/subminer', 'set-window', 'set-ready', 'set-init', 'set-ext']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensure yomitan loader returns existing extension when available', async () => {
|
||||||
|
const extension = { id: 'ext' } as never;
|
||||||
|
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||||
|
getYomitanExtension: () => extension,
|
||||||
|
getLoadInFlight: () => null,
|
||||||
|
setLoadInFlight: () => {
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
loadYomitanExtension: async () => {
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await ensureLoaded(), extension);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensure yomitan loader reuses in-flight promise', async () => {
|
||||||
|
const extension = { id: 'ext' } as never;
|
||||||
|
const inflight = Promise.resolve(extension);
|
||||||
|
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||||
|
getYomitanExtension: () => null,
|
||||||
|
getLoadInFlight: () => inflight,
|
||||||
|
setLoadInFlight: () => {
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
loadYomitanExtension: async () => {
|
||||||
|
throw new Error('unexpected');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await ensureLoaded(), extension);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensure yomitan loader starts load and clears in-flight when done', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let inFlight: Promise<any> | null = null;
|
||||||
|
const extension = { id: 'ext' } as never;
|
||||||
|
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||||
|
getYomitanExtension: () => null,
|
||||||
|
getLoadInFlight: () => inFlight,
|
||||||
|
setLoadInFlight: (promise) => {
|
||||||
|
inFlight = promise;
|
||||||
|
calls.push(promise ? 'set:promise' : 'set:null');
|
||||||
|
},
|
||||||
|
loadYomitanExtension: async () => {
|
||||||
|
calls.push('load');
|
||||||
|
return extension;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await ensureLoaded(), extension);
|
||||||
|
assert.deepEqual(calls, ['load', 'set:promise', 'set:null']);
|
||||||
|
});
|
||||||
48
src/main/runtime/yomitan-extension-loader.ts
Normal file
48
src/main/runtime/yomitan-extension-loader.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Extension } from 'electron';
|
||||||
|
import type { YomitanExtensionLoaderDeps } from '../../core/services/yomitan-extension-loader';
|
||||||
|
|
||||||
|
export function createLoadYomitanExtensionHandler(deps: {
|
||||||
|
loadYomitanExtensionCore: (options: YomitanExtensionLoaderDeps) => Promise<Extension | null>;
|
||||||
|
userDataPath: YomitanExtensionLoaderDeps['userDataPath'];
|
||||||
|
getYomitanParserWindow: YomitanExtensionLoaderDeps['getYomitanParserWindow'];
|
||||||
|
setYomitanParserWindow: YomitanExtensionLoaderDeps['setYomitanParserWindow'];
|
||||||
|
setYomitanParserReadyPromise: YomitanExtensionLoaderDeps['setYomitanParserReadyPromise'];
|
||||||
|
setYomitanParserInitPromise: YomitanExtensionLoaderDeps['setYomitanParserInitPromise'];
|
||||||
|
setYomitanExtension: YomitanExtensionLoaderDeps['setYomitanExtension'];
|
||||||
|
}) {
|
||||||
|
return async (): Promise<Extension | null> => {
|
||||||
|
return deps.loadYomitanExtensionCore({
|
||||||
|
userDataPath: deps.userDataPath,
|
||||||
|
getYomitanParserWindow: deps.getYomitanParserWindow,
|
||||||
|
setYomitanParserWindow: deps.setYomitanParserWindow,
|
||||||
|
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
|
||||||
|
setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
|
||||||
|
setYomitanExtension: deps.setYomitanExtension,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEnsureYomitanExtensionLoadedHandler(deps: {
|
||||||
|
getYomitanExtension: () => Extension | null;
|
||||||
|
getLoadInFlight: () => Promise<Extension | null> | null;
|
||||||
|
setLoadInFlight: (promise: Promise<Extension | null> | null) => void;
|
||||||
|
loadYomitanExtension: () => Promise<Extension | null>;
|
||||||
|
}) {
|
||||||
|
return async (): Promise<Extension | null> => {
|
||||||
|
const existing = deps.getYomitanExtension();
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inFlight = deps.getLoadInFlight();
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = deps.loadYomitanExtension().finally(() => {
|
||||||
|
deps.setLoadInFlight(null);
|
||||||
|
});
|
||||||
|
deps.setLoadInFlight(promise);
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user