diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index 617c919..741262c 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -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-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-20T08:00:02Z` | +-02-20T08:44:42Z` | | `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-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` | diff --git a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md index 7c1c070..1001d7b 100644 --- a/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md +++ b/docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md @@ -9,6 +9,22 @@ ## Current Work (newest first) +- [2026-02-20T08:34:16Z] progress: extracted field-grouping resolver deps assembly into `src/main/runtime/field-grouping-resolver-main-deps.ts` and rewired `getFieldGroupingResolver`/`setFieldGroupingResolver` handler construction in `src/main.ts`. +- [2026-02-20T08:34:16Z] progress: extracted Yomitan extension loader deps assembly into `src/main/runtime/yomitan-extension-loader-main-deps.ts` and rewired `loadYomitanExtension`/`ensureYomitanExtensionLoaded` handler construction in `src/main.ts`. +- [2026-02-20T08:34:16Z] progress: added parity tests in `src/main/runtime/field-grouping-resolver-main-deps.test.ts` and `src/main/runtime/yomitan-extension-loader-main-deps.test.ts`; `src/main.ts` now 2993 LOC. +- [2026-02-20T08:34:16Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/yomitan-extension-loader-main-deps.test.js dist/main/runtime/yomitan-extension-loader.test.js dist/main/runtime/field-grouping-resolver-main-deps.test.js dist/main/runtime/anilist-token-refresh-main-deps.test.js` pass (7/7). +- [2026-02-20T08:31:52Z] progress: extracted AniList media-state deps assembly into `src/main/runtime/anilist-media-state-main-deps.ts` and rewired media-key/media-guess state handlers in `src/main.ts`. +- [2026-02-20T08:31:52Z] progress: extracted subtitle-position deps assembly into `src/main/runtime/subtitle-position-main-deps.ts` and rewired load/save subtitle position handlers in `src/main.ts`. +- [2026-02-20T08:31:52Z] progress: extracted AniList token-refresh deps assembly into `src/main/runtime/anilist-token-refresh-main-deps.ts` and rewired `refreshAnilistClientSecretState` handler construction in `src/main.ts`. +- [2026-02-20T08:31:52Z] progress: added parity tests in `src/main/runtime/anilist-media-state-main-deps.test.ts`, `src/main/runtime/subtitle-position-main-deps.test.ts`, and `src/main/runtime/anilist-token-refresh-main-deps.test.ts`; `src/main.ts` now 2968 LOC. +- [2026-02-20T08:31:52Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/anilist-token-refresh-main-deps.test.js dist/main/runtime/anilist-token-refresh.test.js dist/main/runtime/anilist-media-state-main-deps.test.js dist/main/runtime/subtitle-position-main-deps.test.js dist/main/runtime/jellyfin-subtitle-preload-main-deps.test.js` pass (12/12). +- [2026-02-20T08:12:54Z] progress: extracted Jellyfin subtitle-preload deps assembly into `src/main/runtime/jellyfin-subtitle-preload-main-deps.ts` and rewired `preloadJellyfinExternalSubtitles` construction in `src/main.ts`. +- [2026-02-20T08:12:54Z] progress: added parity tests in `src/main/runtime/jellyfin-subtitle-preload-main-deps.test.ts`; `src/main.ts` now 2926 LOC. +- [2026-02-20T08:12:54Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-subtitle-preload-main-deps.test.js dist/main/runtime/jellyfin-subtitle-preload.test.js dist/main/runtime/jellyfin-remote-session-main-deps.test.js` pass (6/6). +- [2026-02-20T08:11:29Z] progress: committed checkpoint `a85b6c2` (`refactor: extract additional main runtime dependency builders`) and continued with Jellyfin remote-session deps extraction. +- [2026-02-20T08:11:29Z] progress: extracted start/stop Jellyfin remote-session deps assembly into `src/main/runtime/jellyfin-remote-session-main-deps.ts` and rewired those constructor sites in `src/main.ts`. +- [2026-02-20T08:11:29Z] progress: added parity tests in `src/main/runtime/jellyfin-remote-session-main-deps.test.ts`; `src/main.ts` now 2921 LOC. +- [2026-02-20T08:11:29Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-remote-session-main-deps.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-command-dispatch-main-deps.test.js` pass (5/5). - [2026-02-20T08:00:02Z] progress: extracted Jellyfin command-dispatch deps assembly into `src/main/runtime/jellyfin-command-dispatch-main-deps.ts` (`createBuildRunJellyfinCommandMainDepsHandler`) and rewired `runJellyfinCommand` construction in `src/main.ts`. - [2026-02-20T08:00:02Z] progress: added parity tests in `src/main/runtime/jellyfin-command-dispatch-main-deps.test.ts`; `src/main.ts` now 2909 LOC. - [2026-02-20T08:00:02Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-command-dispatch-main-deps.test.js dist/main/runtime/jellyfin-command-dispatch.test.js dist/main/runtime/jellyfin-cli-main-deps.test.js` pass (8/8). @@ -353,3 +369,24 @@ - [2026-02-20T07:27:15Z] progress: extracted overlay-shortcuts runtime deps assembly into `src/main/runtime/overlay-shortcuts-runtime-main-deps.ts` and rewired `createOverlayShortcutsRuntimeService` setup in `src/main.ts` through the builder. - [2026-02-20T07:27:15Z] progress: `src/main.ts` currently 2750 LOC after this slice. - [2026-02-20T07:27:15Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-shortcuts-runtime-main-deps.test.js dist/main/overlay-shortcuts-runtime.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js` pass (5/5). +- [2026-02-20T08:39:19Z] progress: extracted setup-window dependency assembly from `src/main.ts` into `src/main/runtime/anilist-setup-window-main-deps.ts` and `src/main/runtime/jellyfin-setup-window-main-deps.ts`; rewired `openAnilistSetupWindow` + `openJellyfinSetupWindow` to builder-backed handlers. +- [2026-02-20T08:39:19Z] progress: added builder mapping tests in `src/main/runtime/anilist-setup-window-main-deps.test.ts` and `src/main/runtime/jellyfin-setup-window-main-deps.test.ts`. +- [2026-02-20T08:39:19Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/anilist-setup-window-main-deps.test.js dist/main/runtime/jellyfin-setup-window-main-deps.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js` pass. +- [2026-02-20T08:44:42Z] progress: extracted AniList media-guess/post-watch dependency assemblies into `src/main/runtime/anilist-media-guess-main-deps.ts` + `src/main/runtime/anilist-post-watch-main-deps.ts`; rewired `main.ts` (`maybeProbeAnilistDuration`, `ensureAnilistMediaGuess`, `processNextAnilistRetryUpdate`, `maybeRunAnilistPostWatchUpdate`) to builder-backed setup. +- [2026-02-20T08:44:42Z] progress: extracted Anki/mining action dependency assemblies into `src/main/runtime/anki-actions-main-deps.ts` + `src/main/runtime/mining-actions-main-deps.ts`; rewired corresponding handler creation block in `main.ts`. +- [2026-02-20T08:44:42Z] progress: added mapping tests for all new builders (`anilist-media-guess-main-deps`, `anilist-post-watch-main-deps`, `anki-actions-main-deps`, `mining-actions-main-deps`). +- [2026-02-20T08:44:42Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/anilist-media-guess-main-deps.test.js dist/main/runtime/anilist-post-watch-main-deps.test.js dist/main/runtime/anilist-media-guess.test.js dist/main/runtime/anilist-post-watch.test.js dist/main/runtime/anki-actions-main-deps.test.js dist/main/runtime/mining-actions-main-deps.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/mining-actions.test.js` pass. +- [2026-02-20T08:52:08Z] progress: extracted overlay visibility/main action and IPC-bridge dependency assembly from `main.ts` into new builders: `overlay-visibility-actions-main-deps.ts`, `overlay-main-actions-main-deps.ts`, `ipc-bridge-actions-main-deps.ts`. +- [2026-02-20T08:52:08Z] progress: rewired `main.ts` handler setup for `set/toggle overlay`, `appendClipboardVideoToQueue`, `handleMpvCommandFromIpc`, and `runSubsyncManualFromIpc` to builder-backed assembly. +- [2026-02-20T08:52:08Z] progress: added builder mapping tests: `overlay-visibility-actions-main-deps.test.ts`, `overlay-main-actions-main-deps.test.ts`, `ipc-bridge-actions-main-deps.test.ts`. +- [2026-02-20T08:52:08Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/ipc-bridge-actions-main-deps.test.js dist/main/runtime/overlay-visibility-actions-main-deps.test.js dist/main/runtime/overlay-main-actions-main-deps.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/overlay-main-actions.test.js` pass. +- [2026-02-20T08:54:17Z] progress: extracted numeric shortcut and overlay-shortcuts lifecycle dependency assembly into `numeric-shortcut-session-main-deps.ts` and `overlay-shortcuts-lifecycle-main-deps.ts`; rewired corresponding `main.ts` setup blocks. +- [2026-02-20T08:54:17Z] progress: extracted overlay-window-layout dependency assembly into `overlay-window-layout-main-deps.ts`; rewired visible/invisible bounds + window level/order setup in `main.ts`. +- [2026-02-20T08:54:17Z] progress: added mapping tests for all new builders (`numeric-shortcut-session-main-deps`, `overlay-shortcuts-lifecycle-main-deps`, `overlay-window-layout-main-deps`). +- [2026-02-20T08:54:17Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/numeric-shortcut-session-main-deps.test.js dist/main/runtime/overlay-shortcuts-lifecycle-main-deps.test.js dist/main/runtime/overlay-window-layout-main-deps.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/overlay-window-layout.test.js` pass. +- [2026-02-20T08:55:21Z] progress: extracted startup warmup dependency assembly into `src/main/runtime/startup-warmups-main-deps.ts` (`launchBackgroundWarmupTask`, `startBackgroundWarmups`) and rewired main setup to builder-backed constants. +- [2026-02-20T08:55:21Z] progress: added `src/main/runtime/startup-warmups-main-deps.test.ts` mapping coverage. +- [2026-02-20T08:55:21Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/startup-warmups-main-deps.test.js dist/main/runtime/startup-warmups.test.js` pass. +- [2026-02-20T08:56:46Z] progress: extracted MPV IPC command dependency assembly into `src/main/runtime/ipc-mpv-command-main-deps.ts` and rewired `main.ts` IPC bridge setup to compose through `buildMpvCommandFromIpcRuntimeMainDepsHandler`. +- [2026-02-20T08:56:46Z] progress: added `src/main/runtime/ipc-mpv-command-main-deps.test.ts` mapping coverage. +- [2026-02-20T08:56:46Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/ipc-mpv-command-main-deps.test.js dist/main/runtime/ipc-bridge-actions-main-deps.test.js dist/main/runtime/ipc-bridge-actions.test.js` pass. diff --git a/src/main.ts b/src/main.ts index 7d7769c..69c6667 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,6 +101,7 @@ import { createBuildRegisterSubminerProtocolClientMainDepsHandler, } from './main/runtime/anilist-setup-protocol-main-deps'; import { createRefreshAnilistClientSecretStateHandler } from './main/runtime/anilist-token-refresh'; +import { createBuildRefreshAnilistClientSecretStateMainDepsHandler } from './main/runtime/anilist-token-refresh-main-deps'; import { createHandleJellyfinRemoteGeneralCommand, createHandleJellyfinRemotePlay, @@ -146,14 +147,20 @@ import { createMaybeFocusExistingJellyfinSetupWindowHandler, parseJellyfinSetupSubmissionUrl, } from './main/runtime/jellyfin-setup-window'; +import { createBuildOpenJellyfinSetupWindowMainDepsHandler } from './main/runtime/jellyfin-setup-window-main-deps'; import { createMaybeFocusExistingAnilistSetupWindowHandler, createOpenAnilistSetupWindowHandler, } from './main/runtime/anilist-setup-window'; +import { createBuildOpenAnilistSetupWindowMainDepsHandler } from './main/runtime/anilist-setup-window-main-deps'; import { createEnsureAnilistMediaGuessHandler, createMaybeProbeAnilistDurationHandler, } from './main/runtime/anilist-media-guess'; +import { + createBuildEnsureAnilistMediaGuessMainDepsHandler, + createBuildMaybeProbeAnilistDurationMainDepsHandler, +} from './main/runtime/anilist-media-guess-main-deps'; import { createGetAnilistMediaGuessRuntimeStateHandler, createGetCurrentAnilistMediaKeyHandler, @@ -161,19 +168,35 @@ import { createResetAnilistMediaTrackingHandler, createSetAnilistMediaGuessRuntimeStateHandler, } from './main/runtime/anilist-media-state'; +import { + createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler, + createBuildGetCurrentAnilistMediaKeyMainDepsHandler, + createBuildResetAnilistMediaGuessStateMainDepsHandler, + createBuildResetAnilistMediaTrackingMainDepsHandler, + createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler, +} from './main/runtime/anilist-media-state-main-deps'; import { buildAnilistAttemptKey, createMaybeRunAnilistPostWatchUpdateHandler, createProcessNextAnilistRetryUpdateHandler, rememberAnilistAttemptedUpdateKey, } from './main/runtime/anilist-post-watch'; +import { + createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler, + createBuildProcessNextAnilistRetryUpdateMainDepsHandler, +} from './main/runtime/anilist-post-watch-main-deps'; import { createLoadSubtitlePositionHandler, createSaveSubtitlePositionHandler, } from './main/runtime/subtitle-position'; +import { + createBuildLoadSubtitlePositionMainDepsHandler, + createBuildSaveSubtitlePositionMainDepsHandler, +} from './main/runtime/subtitle-position-main-deps'; import { registerProtocolUrlHandlers } from './main/runtime/protocol-url-handlers'; import { createHandleJellyfinAuthCommands } from './main/runtime/jellyfin-cli-auth'; import { createRunJellyfinCommandHandler } from './main/runtime/jellyfin-command-dispatch'; +import { createBuildRunJellyfinCommandMainDepsHandler } from './main/runtime/jellyfin-command-dispatch-main-deps'; import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list'; import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play'; import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce'; @@ -183,7 +206,6 @@ import { createBuildHandleJellyfinPlayCommandMainDepsHandler, createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler, } from './main/runtime/jellyfin-cli-main-deps'; -import { createBuildRunJellyfinCommandMainDepsHandler } from './main/runtime/jellyfin-command-dispatch-main-deps'; import { createGetJellyfinClientInfoHandler, createGetResolvedJellyfinConfigHandler, @@ -209,10 +231,15 @@ import { } from './main/runtime/dictionary-runtime-main-deps'; import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch'; import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload'; +import { createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler } from './main/runtime/jellyfin-subtitle-preload-main-deps'; import { createStartJellyfinRemoteSessionHandler, createStopJellyfinRemoteSessionHandler, } from './main/runtime/jellyfin-remote-session-lifecycle'; +import { + createBuildStartJellyfinRemoteSessionMainDepsHandler, + createBuildStopJellyfinRemoteSessionMainDepsHandler, +} from './main/runtime/jellyfin-remote-session-main-deps'; import { createHandleInitialArgsHandler } from './main/runtime/initial-args-handler'; import { createBuildHandleInitialArgsMainDepsHandler } from './main/runtime/initial-args-main-deps'; import { createHandleTexthookerOnlyModeTransitionHandler } from './main/runtime/cli-command-prechecks'; @@ -221,6 +248,10 @@ import { createGetFieldGroupingResolverHandler, createSetFieldGroupingResolverHandler, } from './main/runtime/field-grouping-resolver'; +import { + createBuildGetFieldGroupingResolverMainDepsHandler, + createBuildSetFieldGroupingResolverMainDepsHandler, +} from './main/runtime/field-grouping-resolver-main-deps'; import { createBuildFieldGroupingOverlayMainDepsHandler } from './main/runtime/field-grouping-overlay-main-deps'; import { createCliCommandContext } from './main/runtime/cli-command-context'; import { createBindMpvMainEventHandlersHandler } from './main/runtime/mpv-main-event-bindings'; @@ -237,12 +268,22 @@ import { createLaunchBackgroundWarmupTaskHandler, createStartBackgroundWarmupsHandler, } from './main/runtime/startup-warmups'; +import { + createBuildLaunchBackgroundWarmupTaskMainDepsHandler, + createBuildStartBackgroundWarmupsMainDepsHandler, +} from './main/runtime/startup-warmups-main-deps'; import { createEnforceOverlayLayerOrderHandler, createEnsureOverlayWindowLevelHandler, createUpdateInvisibleOverlayBoundsHandler, createUpdateVisibleOverlayBoundsHandler, } from './main/runtime/overlay-window-layout'; +import { + createBuildEnforceOverlayLayerOrderMainDepsHandler, + createBuildEnsureOverlayWindowLevelMainDepsHandler, + createBuildUpdateInvisibleOverlayBoundsMainDepsHandler, + createBuildUpdateVisibleOverlayBoundsMainDepsHandler, +} from './main/runtime/overlay-window-layout-main-deps'; import { buildTrayMenuTemplateRuntime, resolveTrayIconPathRuntime } from './main/runtime/tray-runtime'; import { createDestroyTrayHandler, createEnsureTrayHandler } from './main/runtime/tray-lifecycle'; import { createInitializeOverlayRuntimeHandler } from './main/runtime/overlay-runtime-bootstrap'; @@ -267,12 +308,22 @@ import { createCancelNumericShortcutSessionHandler, createStartNumericShortcutSessionHandler, } from './main/runtime/numeric-shortcut-session-handlers'; +import { + createBuildCancelNumericShortcutSessionMainDepsHandler, + createBuildStartNumericShortcutSessionMainDepsHandler, +} from './main/runtime/numeric-shortcut-session-main-deps'; import { createRefreshOverlayShortcutsHandler, createRegisterOverlayShortcutsHandler, createSyncOverlayShortcutsHandler, createUnregisterOverlayShortcutsHandler, } from './main/runtime/overlay-shortcuts-lifecycle'; +import { + createBuildRefreshOverlayShortcutsMainDepsHandler, + createBuildRegisterOverlayShortcutsMainDepsHandler, + createBuildSyncOverlayShortcutsMainDepsHandler, + createBuildUnregisterOverlayShortcutsMainDepsHandler, +} from './main/runtime/overlay-shortcuts-lifecycle-main-deps'; import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/overlay-shortcuts-runtime-main-deps'; import { createMarkLastCardAsAudioCardHandler, @@ -281,17 +332,35 @@ import { createTriggerFieldGroupingHandler, createUpdateLastCardFromClipboardHandler, } from './main/runtime/anki-actions'; +import { + createBuildMarkLastCardAsAudioCardMainDepsHandler, + createBuildMineSentenceCardMainDepsHandler, + createBuildRefreshKnownWordCacheMainDepsHandler, + createBuildTriggerFieldGroupingMainDepsHandler, + createBuildUpdateLastCardFromClipboardMainDepsHandler, +} from './main/runtime/anki-actions-main-deps'; import { createCopyCurrentSubtitleHandler, createHandleMineSentenceDigitHandler, createHandleMultiCopyDigitHandler, } from './main/runtime/mining-actions'; +import { + createBuildCopyCurrentSubtitleMainDepsHandler, + createBuildHandleMineSentenceDigitMainDepsHandler, + createBuildHandleMultiCopyDigitMainDepsHandler, +} from './main/runtime/mining-actions-main-deps'; import { createSetInvisibleOverlayVisibleHandler, createSetVisibleOverlayVisibleHandler, createToggleInvisibleOverlayHandler, createToggleVisibleOverlayHandler, } from './main/runtime/overlay-visibility-actions'; +import { + createBuildSetInvisibleOverlayVisibleMainDepsHandler, + createBuildSetVisibleOverlayVisibleMainDepsHandler, + createBuildToggleInvisibleOverlayMainDepsHandler, + createBuildToggleVisibleOverlayMainDepsHandler, +} from './main/runtime/overlay-visibility-actions-main-deps'; import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './main/runtime/overlay-visibility-runtime-main-deps'; import { createAppendClipboardVideoToQueueHandler, @@ -299,6 +368,12 @@ import { createSetOverlayVisibleHandler, createToggleOverlayHandler, } from './main/runtime/overlay-main-actions'; +import { + createBuildAppendClipboardVideoToQueueMainDepsHandler, + createBuildHandleOverlayModalClosedMainDepsHandler, + createBuildSetOverlayVisibleMainDepsHandler, + createBuildToggleOverlayMainDepsHandler, +} from './main/runtime/overlay-main-actions-main-deps'; import { createBroadcastRuntimeOptionsChangedHandler, createGetRuntimeOptionsStateHandler, @@ -319,6 +394,11 @@ import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler, } from './main/runtime/ipc-bridge-actions'; +import { + createBuildHandleMpvCommandFromIpcMainDepsHandler, + createBuildRunSubsyncManualFromIpcMainDepsHandler, +} from './main/runtime/ipc-bridge-actions-main-deps'; +import { createBuildMpvCommandFromIpcRuntimeMainDepsHandler } from './main/runtime/ipc-mpv-command-main-deps'; import { createCreateInvisibleWindowHandler, createCreateMainWindowHandler, @@ -347,6 +427,10 @@ import { createEnsureYomitanExtensionLoadedHandler, createLoadYomitanExtensionHandler, } from './main/runtime/yomitan-extension-loader'; +import { + createBuildEnsureYomitanExtensionLoadedMainDepsHandler, + createBuildLoadYomitanExtensionMainDepsHandler, +} from './main/runtime/yomitan-extension-loader-main-deps'; import { createBuildInitializeOverlayRuntimeOptionsHandler } from './main/runtime/overlay-runtime-options'; import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './main/runtime/overlay-runtime-options-main-deps'; import { createBuildCliCommandContextDepsHandler } from './main/runtime/cli-command-context-deps'; @@ -914,15 +998,21 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService( })(), ); -const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler({ +const buildGetFieldGroupingResolverMainDepsHandler = createBuildGetFieldGroupingResolverMainDepsHandler( + { getResolver: () => appState.fieldGroupingResolver, -}); + }, +); +const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler( + buildGetFieldGroupingResolverMainDepsHandler(), +); function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null { return getFieldGroupingResolverHandler(); } -const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler({ +const buildSetFieldGroupingResolverMainDepsHandler = createBuildSetFieldGroupingResolverMainDepsHandler( + { setResolver: (resolver) => { appState.fieldGroupingResolver = resolver; }, @@ -931,7 +1021,11 @@ const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler({ return appState.fieldGroupingResolverSequence; }, getSequence: () => appState.fieldGroupingResolverSequence, -}); + }, +); +const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler( + buildSetFieldGroupingResolverMainDepsHandler(), +); function setFieldGroupingResolver( resolver: ((choice: KikuFieldGroupingChoice) => void) | null, @@ -1183,18 +1277,22 @@ const ensureMpvConnectedForJellyfinPlayback = createEnsureMpvConnectedForJellyfi buildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler(), ); -const preloadJellyfinExternalSubtitles = createPreloadJellyfinExternalSubtitlesHandler({ - listJellyfinSubtitleTracks: (session, clientInfo, itemId) => - listJellyfinSubtitleTracksRuntime(session, clientInfo, itemId), - getMpvClient: () => appState.mpvClient, - sendMpvCommand: (command) => { - sendMpvCommandRuntime(appState.mpvClient, command); - }, - wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - logDebug: (message, error) => { - logger.debug(message, error); - }, -}); +const buildPreloadJellyfinExternalSubtitlesMainDepsHandler = + createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler({ + listJellyfinSubtitleTracks: (session, clientInfo, itemId) => + listJellyfinSubtitleTracksRuntime(session, clientInfo, itemId), + getMpvClient: () => appState.mpvClient, + sendMpvCommand: (command) => { + sendMpvCommandRuntime(appState.mpvClient, command); + }, + wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), + logDebug: (message, error) => { + logger.debug(message, error); + }, + }); +const preloadJellyfinExternalSubtitles = createPreloadJellyfinExternalSubtitlesHandler( + buildPreloadJellyfinExternalSubtitlesMainDepsHandler(), +); const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler({ ensureMpvConnectedForPlayback: () => ensureMpvConnectedForJellyfinPlayback(), @@ -1288,7 +1386,8 @@ const handleJellyfinRemoteAnnounceCommand = createHandleJellyfinRemoteAnnounceCo buildHandleJellyfinRemoteAnnounceCommandMainDepsHandler(), ); -const startJellyfinRemoteSession = createStartJellyfinRemoteSessionHandler({ +const buildStartJellyfinRemoteSessionMainDepsHandler = + createBuildStartJellyfinRemoteSessionMainDepsHandler({ getJellyfinConfig: () => getResolvedJellyfinConfig(), getCurrentSession: () => appState.jellyfinRemoteSession, setCurrentSession: (session) => { @@ -1304,8 +1403,12 @@ const startJellyfinRemoteSession = createStartJellyfinRemoteSessionHandler({ logInfo: (message) => logger.info(message), logWarn: (message, details) => logger.warn(message, details), }); +const startJellyfinRemoteSession = createStartJellyfinRemoteSessionHandler( + buildStartJellyfinRemoteSessionMainDepsHandler(), +); -const stopJellyfinRemoteSession = createStopJellyfinRemoteSessionHandler({ +const buildStopJellyfinRemoteSessionMainDepsHandler = + createBuildStopJellyfinRemoteSessionMainDepsHandler({ getCurrentSession: () => appState.jellyfinRemoteSession, setCurrentSession: (session) => { appState.jellyfinRemoteSession = session as typeof appState.jellyfinRemoteSession; @@ -1314,6 +1417,9 @@ const stopJellyfinRemoteSession = createStopJellyfinRemoteSessionHandler({ activeJellyfinRemotePlayback = null; }, }); +const stopJellyfinRemoteSession = createStopJellyfinRemoteSessionHandler( + buildStopJellyfinRemoteSessionMainDepsHandler(), +); const buildRunJellyfinCommandMainDepsHandler = createBuildRunJellyfinCommandMainDepsHandler({ getJellyfinConfig: () => getResolvedJellyfinConfig(), @@ -1395,66 +1501,68 @@ const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandl buildRegisterSubminerProtocolClientMainDepsHandler(), ); -function openAnilistSetupWindow(): void { - createOpenAnilistSetupWindowHandler({ - maybeFocusExistingSetupWindow: createMaybeFocusExistingAnilistSetupWindowHandler({ - getSetupWindow: () => appState.anilistSetupWindow, +const buildOpenAnilistSetupWindowMainDepsHandler = createBuildOpenAnilistSetupWindowMainDepsHandler({ + maybeFocusExistingSetupWindow: createMaybeFocusExistingAnilistSetupWindowHandler({ + getSetupWindow: () => appState.anilistSetupWindow, + }), + createSetupWindow: () => + new BrowserWindow({ + width: 1000, + height: 760, + title: 'Anilist Setup', + show: true, + autoHideMenuBar: true, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + }, }), - createSetupWindow: () => - new BrowserWindow({ - width: 1000, - height: 760, - title: 'Anilist Setup', - show: true, - autoHideMenuBar: true, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - }, - }), - buildAuthorizeUrl: () => - buildAnilistSetupUrl({ - authorizeUrl: ANILIST_SETUP_CLIENT_ID_URL, - clientId: ANILIST_DEFAULT_CLIENT_ID, - responseType: ANILIST_SETUP_RESPONSE_TYPE, - }), - consumeCallbackUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), - openSetupInBrowser: (authorizeUrl) => - openAnilistSetupInBrowser({ - authorizeUrl, - openExternal: (url) => shell.openExternal(url), - logError: (message, error) => logger.error(message, error), - }), - loadManualTokenEntry: (setupWindow, authorizeUrl) => - loadAnilistManualTokenEntry({ - setupWindow: setupWindow as BrowserWindow, - authorizeUrl, - developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, - logWarn: (message, data) => logger.warn(message, data), - }), - redirectUri: ANILIST_REDIRECT_URI, - developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, - isAllowedExternalUrl: (url) => isAllowedAnilistExternalUrl(url), - isAllowedNavigationUrl: (url) => isAllowedAnilistSetupNavigationUrl(url), - logWarn: (message, details) => logger.warn(message, details), - logError: (message, details) => logger.error(message, details), - clearSetupWindow: () => { - appState.anilistSetupWindow = null; - }, - setSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - setSetupWindow: (setupWindow) => { - appState.anilistSetupWindow = setupWindow as BrowserWindow; - }, - openExternal: (url) => { - void shell.openExternal(url); - }, - })(); + buildAuthorizeUrl: () => + buildAnilistSetupUrl({ + authorizeUrl: ANILIST_SETUP_CLIENT_ID_URL, + clientId: ANILIST_DEFAULT_CLIENT_ID, + responseType: ANILIST_SETUP_RESPONSE_TYPE, + }), + consumeCallbackUrl: (rawUrl) => consumeAnilistSetupTokenFromUrl(rawUrl), + openSetupInBrowser: (authorizeUrl) => + openAnilistSetupInBrowser({ + authorizeUrl, + openExternal: (url) => shell.openExternal(url), + logError: (message, error) => logger.error(message, error), + }), + loadManualTokenEntry: (setupWindow, authorizeUrl) => + loadAnilistManualTokenEntry({ + setupWindow: setupWindow as BrowserWindow, + authorizeUrl, + developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, + logWarn: (message, data) => logger.warn(message, data), + }), + redirectUri: ANILIST_REDIRECT_URI, + developerSettingsUrl: ANILIST_DEVELOPER_SETTINGS_URL, + isAllowedExternalUrl: (url) => isAllowedAnilistExternalUrl(url), + isAllowedNavigationUrl: (url) => isAllowedAnilistSetupNavigationUrl(url), + logWarn: (message, details) => logger.warn(message, details), + logError: (message, details) => logger.error(message, details), + clearSetupWindow: () => { + appState.anilistSetupWindow = null; + }, + setSetupPageOpened: (opened) => { + appState.anilistSetupPageOpened = opened; + }, + setSetupWindow: (setupWindow) => { + appState.anilistSetupWindow = setupWindow as BrowserWindow; + }, + openExternal: (url) => { + void shell.openExternal(url); + }, +}); + +function openAnilistSetupWindow(): void { + createOpenAnilistSetupWindowHandler(buildOpenAnilistSetupWindowMainDepsHandler())(); } -function openJellyfinSetupWindow(): void { - createOpenJellyfinSetupWindowHandler({ +const buildOpenJellyfinSetupWindowMainDepsHandler = + createBuildOpenJellyfinSetupWindowMainDepsHandler({ maybeFocusExistingSetupWindow: createMaybeFocusExistingJellyfinSetupWindowHandler({ getSetupWindow: () => appState.jellyfinSetupWindow, }), @@ -1495,41 +1603,53 @@ function openJellyfinSetupWindow(): void { appState.jellyfinSetupWindow = null; }, setSetupWindow: (window) => { - appState.jellyfinSetupWindow = window; + appState.jellyfinSetupWindow = window as BrowserWindow; }, encodeURIComponent: (value) => encodeURIComponent(value), - })(); + }); + +function openJellyfinSetupWindow(): void { + createOpenJellyfinSetupWindowHandler(buildOpenJellyfinSetupWindowMainDepsHandler())(); } -const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler({ - getResolvedConfig: () => getResolvedConfig(), - isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config), - getCachedAccessToken: () => anilistCachedAccessToken, - setCachedAccessToken: (token) => { - anilistCachedAccessToken = token; - }, - saveStoredToken: (token) => { - anilistTokenStore.saveToken(token); - }, - loadStoredToken: () => anilistTokenStore.loadToken(), - setClientSecretState: (state) => { - anilistStateRuntime.setClientSecretState(state); - }, - getAnilistSetupPageOpened: () => appState.anilistSetupPageOpened, - setAnilistSetupPageOpened: (opened) => { - appState.anilistSetupPageOpened = opened; - }, - openAnilistSetupWindow: () => { - openAnilistSetupWindow(); - }, - now: () => Date.now(), -}); +const buildRefreshAnilistClientSecretStateMainDepsHandler = + createBuildRefreshAnilistClientSecretStateMainDepsHandler({ + getResolvedConfig: () => getResolvedConfig(), + isAnilistTrackingEnabled: (config) => isAnilistTrackingEnabled(config as ResolvedConfig), + getCachedAccessToken: () => anilistCachedAccessToken, + setCachedAccessToken: (token) => { + anilistCachedAccessToken = token; + }, + saveStoredToken: (token) => { + anilistTokenStore.saveToken(token); + }, + loadStoredToken: () => anilistTokenStore.loadToken(), + setClientSecretState: (state) => { + anilistStateRuntime.setClientSecretState(state); + }, + getAnilistSetupPageOpened: () => appState.anilistSetupPageOpened, + setAnilistSetupPageOpened: (opened) => { + appState.anilistSetupPageOpened = opened; + }, + openAnilistSetupWindow: () => { + openAnilistSetupWindow(); + }, + now: () => Date.now(), + }); +const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler( + buildRefreshAnilistClientSecretStateMainDepsHandler(), +); -const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler({ +const buildGetCurrentAnilistMediaKeyMainDepsHandler = + createBuildGetCurrentAnilistMediaKeyMainDepsHandler({ getCurrentMediaPath: () => appState.currentMediaPath, }); +const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler( + buildGetCurrentAnilistMediaKeyMainDepsHandler(), +); -const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler({ +const buildResetAnilistMediaTrackingMainDepsHandler = + createBuildResetAnilistMediaTrackingMainDepsHandler({ setMediaKey: (value) => { anilistCurrentMediaKey = value; }, @@ -1546,16 +1666,24 @@ const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler({ anilistLastDurationProbeAtMs = value; }, }); +const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler( + buildResetAnilistMediaTrackingMainDepsHandler(), +); -const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler({ +const buildGetAnilistMediaGuessRuntimeStateMainDepsHandler = + createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler({ getMediaKey: () => anilistCurrentMediaKey, getMediaDurationSec: () => anilistCurrentMediaDurationSec, getMediaGuess: () => anilistCurrentMediaGuess, getMediaGuessPromise: () => anilistCurrentMediaGuessPromise, getLastDurationProbeAtMs: () => anilistLastDurationProbeAtMs, }); +const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler( + buildGetAnilistMediaGuessRuntimeStateMainDepsHandler(), +); -const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler({ +const buildSetAnilistMediaGuessRuntimeStateMainDepsHandler = + createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler({ setMediaKey: (value) => { anilistCurrentMediaKey = value; }, @@ -1572,8 +1700,12 @@ const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateH anilistLastDurationProbeAtMs = value; }, }); +const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler( + buildSetAnilistMediaGuessRuntimeStateMainDepsHandler(), +); -const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler({ +const buildResetAnilistMediaGuessStateMainDepsHandler = + createBuildResetAnilistMediaGuessStateMainDepsHandler({ setMediaGuess: (value) => { anilistCurrentMediaGuess = value; }, @@ -1581,8 +1713,12 @@ const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler({ anilistCurrentMediaGuessPromise = value; }, }); +const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler( + buildResetAnilistMediaGuessStateMainDepsHandler(), +); -const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler({ +const buildMaybeProbeAnilistDurationMainDepsHandler = + createBuildMaybeProbeAnilistDurationMainDepsHandler({ getState: () => getAnilistMediaGuessRuntimeState(), setState: (state) => { setAnilistMediaGuessRuntimeState(state); @@ -1592,8 +1728,12 @@ const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler({ requestMpvDuration: async () => appState.mpvClient?.requestProperty('duration'), logWarn: (message, error) => logger.warn(message, error), }); +const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler( + buildMaybeProbeAnilistDurationMainDepsHandler(), +); -const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler({ +const buildEnsureAnilistMediaGuessMainDepsHandler = createBuildEnsureAnilistMediaGuessMainDepsHandler( + { getState: () => getAnilistMediaGuessRuntimeState(), setState: (state) => { setAnilistMediaGuessRuntimeState(state); @@ -1602,13 +1742,18 @@ const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler({ getCurrentMediaPath: () => appState.currentMediaPath, getCurrentMediaTitle: () => appState.currentMediaTitle, guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle), -}); +}, +); +const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler( + buildEnsureAnilistMediaGuessMainDepsHandler(), +); const rememberAnilistAttemptedUpdate = (key: string): void => { rememberAnilistAttemptedUpdateKey(anilistAttemptedUpdateKeys, key, ANILIST_MAX_ATTEMPTED_UPDATE_KEYS); }; -const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler({ +const buildProcessNextAnilistRetryUpdateMainDepsHandler = + createBuildProcessNextAnilistRetryUpdateMainDepsHandler({ nextReady: () => anilistUpdateQueue.nextReady(), refreshRetryQueueState: () => anilistStateRuntime.refreshRetryQueueState(), setLastAttemptAt: (value) => { @@ -1632,8 +1777,12 @@ const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler logInfo: (message) => logger.info(message), now: () => Date.now(), }); +const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler( + buildProcessNextAnilistRetryUpdateMainDepsHandler(), +); -const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler({ +const buildMaybeRunAnilistPostWatchUpdateMainDepsHandler = + createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler({ getInFlight: () => anilistUpdateInFlight, setInFlight: (value) => { anilistUpdateInFlight = value; @@ -1673,8 +1822,11 @@ const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandl minWatchSeconds: ANILIST_UPDATE_MIN_WATCH_SECONDS, minWatchRatio: ANILIST_UPDATE_MIN_WATCH_RATIO, }); +const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler( + buildMaybeRunAnilistPostWatchUpdateMainDepsHandler(), +); -const loadSubtitlePosition = createLoadSubtitlePositionHandler({ +const buildLoadSubtitlePositionMainDepsHandler = createBuildLoadSubtitlePositionMainDepsHandler({ loadSubtitlePositionCore: () => loadSubtitlePositionCore({ currentMediaPath: appState.currentMediaPath, @@ -1685,8 +1837,11 @@ const loadSubtitlePosition = createLoadSubtitlePositionHandler({ appState.subtitlePosition = position; }, }); +const loadSubtitlePosition = createLoadSubtitlePositionHandler( + buildLoadSubtitlePositionMainDepsHandler(), +); -const saveSubtitlePosition = createSaveSubtitlePositionHandler({ +const buildSaveSubtitlePositionMainDepsHandler = createBuildSaveSubtitlePositionMainDepsHandler({ saveSubtitlePositionCore: (position) => { saveSubtitlePositionCore({ position, @@ -1704,6 +1859,9 @@ const saveSubtitlePosition = createSaveSubtitlePositionHandler({ appState.subtitlePosition = position; }, }); +const saveSubtitlePosition = createSaveSubtitlePositionHandler( + buildSaveSubtitlePositionMainDepsHandler(), +); registerSubminerProtocolClient(); @@ -2142,13 +2300,18 @@ async function prewarmSubtitleDictionaries(): Promise { await prewarmSubtitleDictionariesHandler(); } -const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler({ +const buildLaunchBackgroundWarmupTaskMainDepsHandler = + createBuildLaunchBackgroundWarmupTaskMainDepsHandler({ now: () => Date.now(), logDebug: (message) => logger.debug(message), logWarn: (message) => logger.warn(message), }); +const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler( + buildLaunchBackgroundWarmupTaskMainDepsHandler(), +); -const startBackgroundWarmups = createStartBackgroundWarmupsHandler({ +const buildStartBackgroundWarmupsMainDepsHandler = createBuildStartBackgroundWarmupsMainDepsHandler( + { getStarted: () => backgroundWarmupsStarted, setStarted: (started) => { backgroundWarmupsStarted = started; @@ -2160,21 +2323,38 @@ const startBackgroundWarmups = createStartBackgroundWarmupsHandler({ prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(), shouldAutoConnectJellyfinRemote: () => getResolvedConfig().jellyfin.remoteControlAutoConnect, startJellyfinRemoteSession: () => startJellyfinRemoteSession(), -}); +}, +); +const startBackgroundWarmups = createStartBackgroundWarmupsHandler( + buildStartBackgroundWarmupsMainDepsHandler(), +); -const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler({ +const buildUpdateVisibleOverlayBoundsMainDepsHandler = + createBuildUpdateVisibleOverlayBoundsMainDepsHandler({ setOverlayWindowBounds: (layer, geometry) => overlayManager.setOverlayWindowBounds(layer, geometry), }); +const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler( + buildUpdateVisibleOverlayBoundsMainDepsHandler(), +); -const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler({ +const buildUpdateInvisibleOverlayBoundsMainDepsHandler = + createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({ setOverlayWindowBounds: (layer, geometry) => overlayManager.setOverlayWindowBounds(layer, geometry), }); +const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler( + buildUpdateInvisibleOverlayBoundsMainDepsHandler(), +); -const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler({ +const buildEnsureOverlayWindowLevelMainDepsHandler = + createBuildEnsureOverlayWindowLevelMainDepsHandler({ ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow), }); +const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler( + buildEnsureOverlayWindowLevelMainDepsHandler(), +); -const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler({ +const buildEnforceOverlayLayerOrderMainDepsHandler = + createBuildEnforceOverlayLayerOrderMainDepsHandler({ enforceOverlayLayerOrderCore: (params) => enforceOverlayLayerOrderCore({ visibleOverlayVisible: params.visibleOverlayVisible, @@ -2189,6 +2369,9 @@ const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler({ getInvisibleWindow: () => overlayManager.getInvisibleWindow(), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow), }); +const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler( + buildEnforceOverlayLayerOrderMainDepsHandler(), +); async function loadYomitanExtension(): Promise { return loadYomitanExtensionHandler(); @@ -2328,10 +2511,16 @@ const numericShortcutRuntime = createNumericShortcutRuntime({ }); const multiCopySession = numericShortcutRuntime.createSession(); const mineSentenceSession = numericShortcutRuntime.createSession(); -const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler({ +const buildCancelPendingMultiCopyMainDepsHandler = + createBuildCancelNumericShortcutSessionMainDepsHandler({ session: multiCopySession, }); -const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler({ +const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler( + buildCancelPendingMultiCopyMainDepsHandler(), +); + +const buildStartPendingMultiCopyMainDepsHandler = + createBuildStartNumericShortcutSessionMainDepsHandler({ session: multiCopySession, onDigit: (count) => handleMultiCopyDigit(count), messages: { @@ -2340,10 +2529,20 @@ const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler({ cancelled: 'Cancelled', }, }); -const cancelPendingMineSentenceMultipleHandler = createCancelNumericShortcutSessionHandler({ +const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler( + buildStartPendingMultiCopyMainDepsHandler(), +); + +const buildCancelPendingMineSentenceMultipleMainDepsHandler = + createBuildCancelNumericShortcutSessionMainDepsHandler({ session: mineSentenceSession, }); -const startPendingMineSentenceMultipleHandler = createStartNumericShortcutSessionHandler({ +const cancelPendingMineSentenceMultipleHandler = createCancelNumericShortcutSessionHandler( + buildCancelPendingMineSentenceMultipleMainDepsHandler(), +); + +const buildStartPendingMineSentenceMultipleMainDepsHandler = + createBuildStartNumericShortcutSessionMainDepsHandler({ session: mineSentenceSession, onDigit: (count) => handleMineSentenceDigit(count), messages: { @@ -2352,18 +2551,40 @@ const startPendingMineSentenceMultipleHandler = createStartNumericShortcutSessio cancelled: 'Cancelled', }, }); -const registerOverlayShortcutsHandler = createRegisterOverlayShortcutsHandler({ +const startPendingMineSentenceMultipleHandler = createStartNumericShortcutSessionHandler( + buildStartPendingMineSentenceMultipleMainDepsHandler(), +); + +const buildRegisterOverlayShortcutsMainDepsHandler = + createBuildRegisterOverlayShortcutsMainDepsHandler({ overlayShortcutsRuntime, }); -const unregisterOverlayShortcutsHandler = createUnregisterOverlayShortcutsHandler({ +const registerOverlayShortcutsHandler = createRegisterOverlayShortcutsHandler( + buildRegisterOverlayShortcutsMainDepsHandler(), +); + +const buildUnregisterOverlayShortcutsMainDepsHandler = + createBuildUnregisterOverlayShortcutsMainDepsHandler({ overlayShortcutsRuntime, }); -const syncOverlayShortcutsHandler = createSyncOverlayShortcutsHandler({ +const unregisterOverlayShortcutsHandler = createUnregisterOverlayShortcutsHandler( + buildUnregisterOverlayShortcutsMainDepsHandler(), +); + +const buildSyncOverlayShortcutsMainDepsHandler = createBuildSyncOverlayShortcutsMainDepsHandler({ overlayShortcutsRuntime, }); -const refreshOverlayShortcutsHandler = createRefreshOverlayShortcutsHandler({ +const syncOverlayShortcutsHandler = createSyncOverlayShortcutsHandler( + buildSyncOverlayShortcutsMainDepsHandler(), +); + +const buildRefreshOverlayShortcutsMainDepsHandler = + createBuildRefreshOverlayShortcutsMainDepsHandler({ overlayShortcutsRuntime, }); +const refreshOverlayShortcutsHandler = createRefreshOverlayShortcutsHandler( + buildRefreshOverlayShortcutsMainDepsHandler(), +); async function triggerSubsyncFromConfig(): Promise { await subsyncRuntime.triggerFromConfig(); @@ -2385,31 +2606,45 @@ function copyCurrentSubtitle(): void { copyCurrentSubtitleHandler(); } -const updateLastCardFromClipboardHandler = createUpdateLastCardFromClipboardHandler({ +const buildUpdateLastCardFromClipboardMainDepsHandler = + createBuildUpdateLastCardFromClipboardMainDepsHandler({ getAnkiIntegration: () => appState.ankiIntegration, readClipboardText: () => clipboard.readText(), showMpvOsd: (text) => showMpvOsd(text), updateLastCardFromClipboardCore, }); +const updateLastCardFromClipboardHandler = createUpdateLastCardFromClipboardHandler( + buildUpdateLastCardFromClipboardMainDepsHandler(), +); -const refreshKnownWordCacheHandler = createRefreshKnownWordCacheHandler({ +const buildRefreshKnownWordCacheMainDepsHandler = createBuildRefreshKnownWordCacheMainDepsHandler({ getAnkiIntegration: () => appState.ankiIntegration, missingIntegrationMessage: 'AnkiConnect integration not enabled', }); +const refreshKnownWordCacheHandler = createRefreshKnownWordCacheHandler( + buildRefreshKnownWordCacheMainDepsHandler(), +); -const triggerFieldGroupingHandler = createTriggerFieldGroupingHandler({ +const buildTriggerFieldGroupingMainDepsHandler = createBuildTriggerFieldGroupingMainDepsHandler({ getAnkiIntegration: () => appState.ankiIntegration, showMpvOsd: (text) => showMpvOsd(text), triggerFieldGroupingCore, }); +const triggerFieldGroupingHandler = createTriggerFieldGroupingHandler( + buildTriggerFieldGroupingMainDepsHandler(), +); -const markLastCardAsAudioCardHandler = createMarkLastCardAsAudioCardHandler({ +const buildMarkLastCardAsAudioCardMainDepsHandler = + createBuildMarkLastCardAsAudioCardMainDepsHandler({ getAnkiIntegration: () => appState.ankiIntegration, showMpvOsd: (text) => showMpvOsd(text), markLastCardAsAudioCardCore, }); +const markLastCardAsAudioCardHandler = createMarkLastCardAsAudioCardHandler( + buildMarkLastCardAsAudioCardMainDepsHandler(), +); -const mineSentenceCardHandler = createMineSentenceCardHandler({ +const buildMineSentenceCardMainDepsHandler = createBuildMineSentenceCardMainDepsHandler({ getAnkiIntegration: () => appState.ankiIntegration, getMpvClient: () => appState.mpvClient, showMpvOsd: (text) => showMpvOsd(text), @@ -2418,19 +2653,30 @@ const mineSentenceCardHandler = createMineSentenceCardHandler({ appState.immersionTracker?.recordCardsMined(count); }, }); -const handleMultiCopyDigitHandler = createHandleMultiCopyDigitHandler({ +const mineSentenceCardHandler = createMineSentenceCardHandler(buildMineSentenceCardMainDepsHandler()); + +const buildHandleMultiCopyDigitMainDepsHandler = createBuildHandleMultiCopyDigitMainDepsHandler({ getSubtitleTimingTracker: () => appState.subtitleTimingTracker, writeClipboardText: (text) => clipboard.writeText(text), showMpvOsd: (text) => showMpvOsd(text), handleMultiCopyDigitCore, }); -const copyCurrentSubtitleHandler = createCopyCurrentSubtitleHandler({ +const handleMultiCopyDigitHandler = createHandleMultiCopyDigitHandler( + buildHandleMultiCopyDigitMainDepsHandler(), +); + +const buildCopyCurrentSubtitleMainDepsHandler = createBuildCopyCurrentSubtitleMainDepsHandler({ getSubtitleTimingTracker: () => appState.subtitleTimingTracker, writeClipboardText: (text) => clipboard.writeText(text), showMpvOsd: (text) => showMpvOsd(text), copyCurrentSubtitleCore, }); -const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler({ +const copyCurrentSubtitleHandler = createCopyCurrentSubtitleHandler( + buildCopyCurrentSubtitleMainDepsHandler(), +); + +const buildHandleMineSentenceDigitMainDepsHandler = + createBuildHandleMineSentenceDigitMainDepsHandler({ getSubtitleTimingTracker: () => appState.subtitleTimingTracker, getAnkiIntegration: () => appState.ankiIntegration, getCurrentSecondarySubText: () => appState.mpvClient?.currentSecondarySubText || undefined, @@ -2443,7 +2689,11 @@ const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler({ }, handleMineSentenceDigitCore, }); -const setVisibleOverlayVisibleHandler = createSetVisibleOverlayVisibleHandler({ +const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler( + buildHandleMineSentenceDigitMainDepsHandler(), +); +const buildSetVisibleOverlayVisibleMainDepsHandler = + createBuildSetVisibleOverlayVisibleMainDepsHandler({ setVisibleOverlayVisibleCore, setVisibleOverlayVisibleState: (nextVisible) => { overlayManager.setVisibleOverlayVisible(nextVisible); @@ -2459,7 +2709,12 @@ const setVisibleOverlayVisibleHandler = createSetVisibleOverlayVisibleHandler({ setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible); }, }); -const setInvisibleOverlayVisibleHandler = createSetInvisibleOverlayVisibleHandler({ +const setVisibleOverlayVisibleHandler = createSetVisibleOverlayVisibleHandler( + buildSetVisibleOverlayVisibleMainDepsHandler(), +); + +const buildSetInvisibleOverlayVisibleMainDepsHandler = + createBuildSetInvisibleOverlayVisibleMainDepsHandler({ setInvisibleOverlayVisibleCore, setInvisibleOverlayVisibleState: (nextVisible) => { overlayManager.setInvisibleOverlayVisible(nextVisible); @@ -2468,24 +2723,49 @@ const setInvisibleOverlayVisibleHandler = createSetInvisibleOverlayVisibleHandle syncInvisibleOverlayMousePassthrough: () => overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(), }); -const toggleVisibleOverlayHandler = createToggleVisibleOverlayHandler({ +const setInvisibleOverlayVisibleHandler = createSetInvisibleOverlayVisibleHandler( + buildSetInvisibleOverlayVisibleMainDepsHandler(), +); + +const buildToggleVisibleOverlayMainDepsHandler = createBuildToggleVisibleOverlayMainDepsHandler({ getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), }); -const toggleInvisibleOverlayHandler = createToggleInvisibleOverlayHandler({ +const toggleVisibleOverlayHandler = createToggleVisibleOverlayHandler( + buildToggleVisibleOverlayMainDepsHandler(), +); + +const buildToggleInvisibleOverlayMainDepsHandler = + createBuildToggleInvisibleOverlayMainDepsHandler({ getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(), setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), }); -const setOverlayVisibleHandler = createSetOverlayVisibleHandler({ +const toggleInvisibleOverlayHandler = createToggleInvisibleOverlayHandler( + buildToggleInvisibleOverlayMainDepsHandler(), +); + +const buildSetOverlayVisibleMainDepsHandler = createBuildSetOverlayVisibleMainDepsHandler({ setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), }); -const toggleOverlayHandler = createToggleOverlayHandler({ +const setOverlayVisibleHandler = createSetOverlayVisibleHandler( + buildSetOverlayVisibleMainDepsHandler(), +); + +const buildToggleOverlayMainDepsHandler = createBuildToggleOverlayMainDepsHandler({ toggleVisibleOverlay: () => toggleVisibleOverlay(), }); -const handleOverlayModalClosedHandler = createHandleOverlayModalClosedHandler({ +const toggleOverlayHandler = createToggleOverlayHandler(buildToggleOverlayMainDepsHandler()); + +const buildHandleOverlayModalClosedMainDepsHandler = + createBuildHandleOverlayModalClosedMainDepsHandler({ handleOverlayModalClosedRuntime: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal), }); -const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHandler({ +const handleOverlayModalClosedHandler = createHandleOverlayModalClosedHandler( + buildHandleOverlayModalClosedMainDepsHandler(), +); + +const buildAppendClipboardVideoToQueueMainDepsHandler = + createBuildAppendClipboardVideoToQueueMainDepsHandler({ appendClipboardVideoToQueueRuntime, getMpvClient: () => appState.mpvClient, readClipboardText: () => clipboard.readText(), @@ -2494,9 +2774,12 @@ const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHand sendMpvCommandRuntime(appState.mpvClient, command); }, }); -const handleMpvCommandFromIpcHandler = createHandleMpvCommandFromIpcHandler({ - handleMpvCommandFromIpcRuntime, - buildMpvCommandDeps: () => ({ +const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHandler( + buildAppendClipboardVideoToQueueMainDepsHandler(), +); + +const buildMpvCommandFromIpcRuntimeMainDepsHandler = + createBuildMpvCommandFromIpcRuntimeMainDepsHandler({ triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(), openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(), cycleRuntimeOption: (id, direction) => { @@ -2515,11 +2798,24 @@ const handleMpvCommandFromIpcHandler = createHandleMpvCommandFromIpcHandler({ sendMpvCommandRuntime(appState.mpvClient, rawCommand), isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected), hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null, - }), + }); + +const buildHandleMpvCommandFromIpcMainDepsHandler = + createBuildHandleMpvCommandFromIpcMainDepsHandler({ + handleMpvCommandFromIpcRuntime, + buildMpvCommandDeps: () => buildMpvCommandFromIpcRuntimeMainDepsHandler(), }); -const runSubsyncManualFromIpcHandler = createRunSubsyncManualFromIpcHandler({ +const handleMpvCommandFromIpcHandler = createHandleMpvCommandFromIpcHandler( + buildHandleMpvCommandFromIpcMainDepsHandler(), +); + +const buildRunSubsyncManualFromIpcMainDepsHandler = + createBuildRunSubsyncManualFromIpcMainDepsHandler({ runManualFromIpc: (request: SubsyncManualRunRequest) => subsyncRuntime.runManualFromIpc(request), }); +const runSubsyncManualFromIpcHandler = createRunSubsyncManualFromIpcHandler( + buildRunSubsyncManualFromIpcMainDepsHandler(), +); const buildCliCommandContextDepsHandler = createBuildCliCommandContextDepsHandler( createBuildCliCommandContextMainDepsHandler({ appState, @@ -2650,7 +2946,7 @@ const destroyTrayHandler = createDestroyTrayHandler( }, })(), ); -const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler({ +const buildLoadYomitanExtensionMainDepsHandler = createBuildLoadYomitanExtensionMainDepsHandler({ loadYomitanExtensionCore, userDataPath: USER_DATA_PATH, getYomitanParserWindow: () => appState.yomitanParserWindow, @@ -2667,14 +2963,21 @@ const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler({ appState.yomitanExt = extension; }, }); -const ensureYomitanExtensionLoadedHandler = createEnsureYomitanExtensionLoadedHandler({ - getYomitanExtension: () => appState.yomitanExt, - getLoadInFlight: () => yomitanLoadInFlight, - setLoadInFlight: (promise) => { - yomitanLoadInFlight = promise; - }, - loadYomitanExtension: () => loadYomitanExtension(), -}); +const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler( + buildLoadYomitanExtensionMainDepsHandler(), +); +const buildEnsureYomitanExtensionLoadedMainDepsHandler = + createBuildEnsureYomitanExtensionLoadedMainDepsHandler({ + getYomitanExtension: () => appState.yomitanExt, + getLoadInFlight: () => yomitanLoadInFlight, + setLoadInFlight: (promise) => { + yomitanLoadInFlight = promise; + }, + loadYomitanExtension: () => loadYomitanExtension(), + }); +const ensureYomitanExtensionLoadedHandler = createEnsureYomitanExtensionLoadedHandler( + buildEnsureYomitanExtensionLoadedMainDepsHandler(), +); const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler( createBuildInitializeOverlayRuntimeMainDepsHandler({ appState, diff --git a/src/main/runtime/anilist-media-guess-main-deps.test.ts b/src/main/runtime/anilist-media-guess-main-deps.test.ts new file mode 100644 index 0000000..2c33486 --- /dev/null +++ b/src/main/runtime/anilist-media-guess-main-deps.test.ts @@ -0,0 +1,78 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildEnsureAnilistMediaGuessMainDepsHandler, + createBuildMaybeProbeAnilistDurationMainDepsHandler, +} from './anilist-media-guess-main-deps'; + +test('maybe probe anilist duration main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildMaybeProbeAnilistDurationMainDepsHandler({ + getState: () => ({ + mediaKey: 'm', + mediaDurationSec: null, + mediaGuess: null, + mediaGuessPromise: null, + lastDurationProbeAtMs: 0, + }), + setState: () => calls.push('set-state'), + durationRetryIntervalMs: 1000, + now: () => 42, + requestMpvDuration: async () => 3600, + logWarn: (message) => calls.push(`warn:${message}`), + })(); + + assert.equal(deps.durationRetryIntervalMs, 1000); + assert.equal(deps.now(), 42); + assert.equal(await deps.requestMpvDuration(), 3600); + deps.setState({ + mediaKey: 'm', + mediaDurationSec: 100, + mediaGuess: null, + mediaGuessPromise: null, + lastDurationProbeAtMs: 0, + }); + deps.logWarn('oops', null); + assert.deepEqual(calls, ['set-state', 'warn:oops']); +}); + +test('ensure anilist media guess main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildEnsureAnilistMediaGuessMainDepsHandler({ + getState: () => ({ + mediaKey: 'm', + mediaDurationSec: null, + mediaGuess: null, + mediaGuessPromise: null, + lastDurationProbeAtMs: 0, + }), + setState: () => calls.push('set-state'), + resolveMediaPathForJimaku: (path) => { + calls.push('resolve'); + return path; + }, + getCurrentMediaPath: () => '/tmp/video.mkv', + getCurrentMediaTitle: () => 'title', + guessAnilistMediaInfo: async () => { + calls.push('guess'); + return { title: 'title', episode: 1, source: 'fallback' }; + }, + })(); + + assert.equal(deps.getCurrentMediaPath(), '/tmp/video.mkv'); + assert.equal(deps.getCurrentMediaTitle(), 'title'); + assert.equal(deps.resolveMediaPathForJimaku('/tmp/video.mkv'), '/tmp/video.mkv'); + assert.deepEqual(await deps.guessAnilistMediaInfo('/tmp/video.mkv', 'title'), { + title: 'title', + episode: 1, + source: 'fallback', + }); + deps.setState({ + mediaKey: 'm', + mediaDurationSec: null, + mediaGuess: null, + mediaGuessPromise: null, + lastDurationProbeAtMs: 0, + }); + assert.deepEqual(calls, ['resolve', 'guess', 'set-state']); +}); diff --git a/src/main/runtime/anilist-media-guess-main-deps.ts b/src/main/runtime/anilist-media-guess-main-deps.ts new file mode 100644 index 0000000..2a5bfeb --- /dev/null +++ b/src/main/runtime/anilist-media-guess-main-deps.ts @@ -0,0 +1,31 @@ +import type { + createEnsureAnilistMediaGuessHandler, + createMaybeProbeAnilistDurationHandler, +} from './anilist-media-guess'; + +type MaybeProbeAnilistDurationMainDeps = Parameters[0]; +type EnsureAnilistMediaGuessMainDeps = Parameters[0]; + +export function createBuildMaybeProbeAnilistDurationMainDepsHandler( + deps: MaybeProbeAnilistDurationMainDeps, +) { + return (): MaybeProbeAnilistDurationMainDeps => ({ + getState: () => deps.getState(), + setState: (state) => deps.setState(state), + durationRetryIntervalMs: deps.durationRetryIntervalMs, + now: () => deps.now(), + requestMpvDuration: () => deps.requestMpvDuration(), + logWarn: (message: string, error: unknown) => deps.logWarn(message, error), + }); +} + +export function createBuildEnsureAnilistMediaGuessMainDepsHandler(deps: EnsureAnilistMediaGuessMainDeps) { + return (): EnsureAnilistMediaGuessMainDeps => ({ + getState: () => deps.getState(), + setState: (state) => deps.setState(state), + resolveMediaPathForJimaku: (currentMediaPath) => deps.resolveMediaPathForJimaku(currentMediaPath), + getCurrentMediaPath: () => deps.getCurrentMediaPath(), + getCurrentMediaTitle: () => deps.getCurrentMediaTitle(), + guessAnilistMediaInfo: (mediaPath, mediaTitle) => deps.guessAnilistMediaInfo(mediaPath, mediaTitle), + }); +} diff --git a/src/main/runtime/anilist-media-state-main-deps.test.ts b/src/main/runtime/anilist-media-state-main-deps.test.ts new file mode 100644 index 0000000..f5515b7 --- /dev/null +++ b/src/main/runtime/anilist-media-state-main-deps.test.ts @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler, + createBuildGetCurrentAnilistMediaKeyMainDepsHandler, + createBuildResetAnilistMediaGuessStateMainDepsHandler, + createBuildResetAnilistMediaTrackingMainDepsHandler, + createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler, +} from './anilist-media-state-main-deps'; + +test('get current anilist media key main deps builder maps callbacks', () => { + const deps = createBuildGetCurrentAnilistMediaKeyMainDepsHandler({ + getCurrentMediaPath: () => '/tmp/video.mkv', + })(); + assert.equal(deps.getCurrentMediaPath(), '/tmp/video.mkv'); +}); + +test('reset anilist media tracking main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildResetAnilistMediaTrackingMainDepsHandler({ + setMediaKey: () => calls.push('key'), + setMediaDurationSec: () => calls.push('duration'), + setMediaGuess: () => calls.push('guess'), + setMediaGuessPromise: () => calls.push('promise'), + setLastDurationProbeAtMs: () => calls.push('probe'), + })(); + deps.setMediaKey(null); + deps.setMediaDurationSec(null); + deps.setMediaGuess(null); + deps.setMediaGuessPromise(null); + deps.setLastDurationProbeAtMs(0); + assert.deepEqual(calls, ['key', 'duration', 'guess', 'promise', 'probe']); +}); + +test('get/set anilist media guess runtime state main deps builders map callbacks', () => { + const getter = createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler({ + getMediaKey: () => '/tmp/video.mkv', + getMediaDurationSec: () => 24, + getMediaGuess: () => ({ title: 'X' }) as never, + getMediaGuessPromise: () => Promise.resolve(null) as never, + getLastDurationProbeAtMs: () => 123, + })(); + assert.equal(getter.getMediaKey(), '/tmp/video.mkv'); + assert.equal(getter.getMediaDurationSec(), 24); + assert.equal(getter.getLastDurationProbeAtMs(), 123); + + const calls: string[] = []; + const setter = createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler({ + setMediaKey: () => calls.push('key'), + setMediaDurationSec: () => calls.push('duration'), + setMediaGuess: () => calls.push('guess'), + setMediaGuessPromise: () => calls.push('promise'), + setLastDurationProbeAtMs: () => calls.push('probe'), + })(); + setter.setMediaKey(null); + setter.setMediaDurationSec(null); + setter.setMediaGuess(null); + setter.setMediaGuessPromise(null); + setter.setLastDurationProbeAtMs(0); + assert.deepEqual(calls, ['key', 'duration', 'guess', 'promise', 'probe']); +}); + +test('reset anilist media guess state main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildResetAnilistMediaGuessStateMainDepsHandler({ + setMediaGuess: () => calls.push('guess'), + setMediaGuessPromise: () => calls.push('promise'), + })(); + deps.setMediaGuess(null); + deps.setMediaGuessPromise(null); + assert.deepEqual(calls, ['guess', 'promise']); +}); diff --git a/src/main/runtime/anilist-media-state-main-deps.ts b/src/main/runtime/anilist-media-state-main-deps.ts new file mode 100644 index 0000000..7057cd2 --- /dev/null +++ b/src/main/runtime/anilist-media-state-main-deps.ts @@ -0,0 +1,72 @@ +import type { + createGetAnilistMediaGuessRuntimeStateHandler, + createGetCurrentAnilistMediaKeyHandler, + createResetAnilistMediaGuessStateHandler, + createResetAnilistMediaTrackingHandler, + createSetAnilistMediaGuessRuntimeStateHandler, +} from './anilist-media-state'; + +type GetCurrentAnilistMediaKeyMainDeps = Parameters[0]; +type ResetAnilistMediaTrackingMainDeps = Parameters[0]; +type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters< + typeof createGetAnilistMediaGuessRuntimeStateHandler +>[0]; +type SetAnilistMediaGuessRuntimeStateMainDeps = Parameters< + typeof createSetAnilistMediaGuessRuntimeStateHandler +>[0]; +type ResetAnilistMediaGuessStateMainDeps = Parameters< + typeof createResetAnilistMediaGuessStateHandler +>[0]; + +export function createBuildGetCurrentAnilistMediaKeyMainDepsHandler( + deps: GetCurrentAnilistMediaKeyMainDeps, +) { + return (): GetCurrentAnilistMediaKeyMainDeps => ({ + getCurrentMediaPath: () => deps.getCurrentMediaPath(), + }); +} + +export function createBuildResetAnilistMediaTrackingMainDepsHandler( + deps: ResetAnilistMediaTrackingMainDeps, +) { + return (): ResetAnilistMediaTrackingMainDeps => ({ + setMediaKey: (value) => deps.setMediaKey(value), + setMediaDurationSec: (value) => deps.setMediaDurationSec(value), + setMediaGuess: (value) => deps.setMediaGuess(value), + setMediaGuessPromise: (value) => deps.setMediaGuessPromise(value), + setLastDurationProbeAtMs: (value: number) => deps.setLastDurationProbeAtMs(value), + }); +} + +export function createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler( + deps: GetAnilistMediaGuessRuntimeStateMainDeps, +) { + return (): GetAnilistMediaGuessRuntimeStateMainDeps => ({ + getMediaKey: () => deps.getMediaKey(), + getMediaDurationSec: () => deps.getMediaDurationSec(), + getMediaGuess: () => deps.getMediaGuess(), + getMediaGuessPromise: () => deps.getMediaGuessPromise(), + getLastDurationProbeAtMs: () => deps.getLastDurationProbeAtMs(), + }); +} + +export function createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler( + deps: SetAnilistMediaGuessRuntimeStateMainDeps, +) { + return (): SetAnilistMediaGuessRuntimeStateMainDeps => ({ + setMediaKey: (value) => deps.setMediaKey(value), + setMediaDurationSec: (value) => deps.setMediaDurationSec(value), + setMediaGuess: (value) => deps.setMediaGuess(value), + setMediaGuessPromise: (value) => deps.setMediaGuessPromise(value), + setLastDurationProbeAtMs: (value: number) => deps.setLastDurationProbeAtMs(value), + }); +} + +export function createBuildResetAnilistMediaGuessStateMainDepsHandler( + deps: ResetAnilistMediaGuessStateMainDeps, +) { + return (): ResetAnilistMediaGuessStateMainDeps => ({ + setMediaGuess: (value) => deps.setMediaGuess(value), + setMediaGuessPromise: (value) => deps.setMediaGuessPromise(value), + }); +} diff --git a/src/main/runtime/anilist-post-watch-main-deps.test.ts b/src/main/runtime/anilist-post-watch-main-deps.test.ts new file mode 100644 index 0000000..bb88e69 --- /dev/null +++ b/src/main/runtime/anilist-post-watch-main-deps.test.ts @@ -0,0 +1,118 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler, + createBuildProcessNextAnilistRetryUpdateMainDepsHandler, +} from './anilist-post-watch-main-deps'; + +test('process next anilist retry update main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildProcessNextAnilistRetryUpdateMainDepsHandler({ + nextReady: () => ({ key: 'k', title: 't', episode: 1 }), + refreshRetryQueueState: () => calls.push('refresh'), + setLastAttemptAt: () => calls.push('attempt'), + setLastError: () => calls.push('error'), + refreshAnilistClientSecretState: async () => 'token', + updateAnilistPostWatchProgress: async () => ({ status: 'updated', message: 'ok' }), + markSuccess: () => calls.push('success'), + rememberAttemptedUpdateKey: () => calls.push('remember'), + markFailure: () => calls.push('failure'), + logInfo: (message) => calls.push(`info:${message}`), + now: () => 7, + })(); + + assert.deepEqual(deps.nextReady(), { key: 'k', title: 't', episode: 1 }); + deps.refreshRetryQueueState(); + deps.setLastAttemptAt(1); + deps.setLastError('x'); + assert.equal(await deps.refreshAnilistClientSecretState(), 'token'); + assert.deepEqual(await deps.updateAnilistPostWatchProgress('token', 't', 1), { + status: 'updated', + message: 'ok', + }); + deps.markSuccess('k'); + deps.rememberAttemptedUpdateKey('k'); + deps.markFailure('k', 'bad'); + deps.logInfo('hello'); + assert.equal(deps.now(), 7); + assert.deepEqual(calls, [ + 'refresh', + 'attempt', + 'error', + 'success', + 'remember', + 'failure', + 'info:hello', + ]); +}); + +test('maybe run anilist post watch update main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler({ + getInFlight: () => false, + setInFlight: () => calls.push('in-flight'), + getResolvedConfig: () => ({}), + isAnilistTrackingEnabled: () => true, + getCurrentMediaKey: () => 'media', + hasMpvClient: () => true, + getTrackedMediaKey: () => 'media', + resetTrackedMedia: () => calls.push('reset'), + getWatchedSeconds: () => 100, + maybeProbeAnilistDuration: async () => 120, + ensureAnilistMediaGuess: async () => ({ title: 'x', episode: 1 }), + hasAttemptedUpdateKey: () => false, + processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }), + refreshAnilistClientSecretState: async () => 'token', + enqueueRetry: () => calls.push('enqueue'), + markRetryFailure: () => calls.push('retry-fail'), + markRetrySuccess: () => calls.push('retry-ok'), + refreshRetryQueueState: () => calls.push('refresh'), + updateAnilistPostWatchProgress: async () => ({ status: 'updated', message: 'done' }), + rememberAttemptedUpdateKey: () => calls.push('remember'), + showMpvOsd: () => calls.push('osd'), + logInfo: (message) => calls.push(`info:${message}`), + logWarn: (message) => calls.push(`warn:${message}`), + minWatchSeconds: 5, + minWatchRatio: 0.5, + })(); + + assert.equal(deps.getInFlight(), false); + deps.setInFlight(true); + assert.equal(deps.isAnilistTrackingEnabled(deps.getResolvedConfig()), true); + assert.equal(deps.getCurrentMediaKey(), 'media'); + assert.equal(deps.hasMpvClient(), true); + assert.equal(deps.getTrackedMediaKey(), 'media'); + deps.resetTrackedMedia('media'); + assert.equal(deps.getWatchedSeconds(), 100); + assert.equal(await deps.maybeProbeAnilistDuration('media'), 120); + assert.deepEqual(await deps.ensureAnilistMediaGuess('media'), { title: 'x', episode: 1 }); + assert.equal(deps.hasAttemptedUpdateKey('k'), false); + assert.deepEqual(await deps.processNextAnilistRetryUpdate(), { ok: true, message: 'ok' }); + assert.equal(await deps.refreshAnilistClientSecretState(), 'token'); + deps.enqueueRetry('k', 't', 1); + deps.markRetryFailure('k', 'bad'); + deps.markRetrySuccess('k'); + deps.refreshRetryQueueState(); + assert.deepEqual(await deps.updateAnilistPostWatchProgress('token', 't', 1), { + status: 'updated', + message: 'done', + }); + deps.rememberAttemptedUpdateKey('k'); + deps.showMpvOsd('ok'); + deps.logInfo('x'); + deps.logWarn('y'); + assert.equal(deps.minWatchSeconds, 5); + assert.equal(deps.minWatchRatio, 0.5); + assert.deepEqual(calls, [ + 'in-flight', + 'reset', + 'enqueue', + 'retry-fail', + 'retry-ok', + 'refresh', + 'remember', + 'osd', + 'info:x', + 'warn:y', + ]); +}); diff --git a/src/main/runtime/anilist-post-watch-main-deps.ts b/src/main/runtime/anilist-post-watch-main-deps.ts new file mode 100644 index 0000000..b5a1a45 --- /dev/null +++ b/src/main/runtime/anilist-post-watch-main-deps.ts @@ -0,0 +1,63 @@ +import type { + createMaybeRunAnilistPostWatchUpdateHandler, + createProcessNextAnilistRetryUpdateHandler, +} from './anilist-post-watch'; + +type ProcessNextAnilistRetryUpdateMainDeps = Parameters< + typeof createProcessNextAnilistRetryUpdateHandler +>[0]; +type MaybeRunAnilistPostWatchUpdateMainDeps = Parameters< + typeof createMaybeRunAnilistPostWatchUpdateHandler +>[0]; + +export function createBuildProcessNextAnilistRetryUpdateMainDepsHandler( + deps: ProcessNextAnilistRetryUpdateMainDeps, +) { + return (): ProcessNextAnilistRetryUpdateMainDeps => ({ + nextReady: () => deps.nextReady(), + refreshRetryQueueState: () => deps.refreshRetryQueueState(), + setLastAttemptAt: (value: number) => deps.setLastAttemptAt(value), + setLastError: (value: string | null) => deps.setLastError(value), + refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(), + updateAnilistPostWatchProgress: (accessToken: string, title: string, episode: number) => + deps.updateAnilistPostWatchProgress(accessToken, title, episode), + markSuccess: (key: string) => deps.markSuccess(key), + rememberAttemptedUpdateKey: (key: string) => deps.rememberAttemptedUpdateKey(key), + markFailure: (key: string, message: string) => deps.markFailure(key, message), + logInfo: (message: string) => deps.logInfo(message), + now: () => deps.now(), + }); +} + +export function createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler( + deps: MaybeRunAnilistPostWatchUpdateMainDeps, +) { + return (): MaybeRunAnilistPostWatchUpdateMainDeps => ({ + getInFlight: () => deps.getInFlight(), + setInFlight: (value: boolean) => deps.setInFlight(value), + getResolvedConfig: () => deps.getResolvedConfig(), + isAnilistTrackingEnabled: (config) => deps.isAnilistTrackingEnabled(config), + getCurrentMediaKey: () => deps.getCurrentMediaKey(), + hasMpvClient: () => deps.hasMpvClient(), + getTrackedMediaKey: () => deps.getTrackedMediaKey(), + resetTrackedMedia: (mediaKey) => deps.resetTrackedMedia(mediaKey), + getWatchedSeconds: () => deps.getWatchedSeconds(), + maybeProbeAnilistDuration: (mediaKey: string) => deps.maybeProbeAnilistDuration(mediaKey), + ensureAnilistMediaGuess: (mediaKey: string) => deps.ensureAnilistMediaGuess(mediaKey), + hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key), + processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(), + refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(), + enqueueRetry: (key: string, title: string, episode: number) => deps.enqueueRetry(key, title, episode), + markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message), + markRetrySuccess: (key: string) => deps.markRetrySuccess(key), + refreshRetryQueueState: () => deps.refreshRetryQueueState(), + updateAnilistPostWatchProgress: (accessToken: string, title: string, episode: number) => + deps.updateAnilistPostWatchProgress(accessToken, title, episode), + rememberAttemptedUpdateKey: (key: string) => deps.rememberAttemptedUpdateKey(key), + showMpvOsd: (message: string) => deps.showMpvOsd(message), + logInfo: (message: string) => deps.logInfo(message), + logWarn: (message: string) => deps.logWarn(message), + minWatchSeconds: deps.minWatchSeconds, + minWatchRatio: deps.minWatchRatio, + }); +} diff --git a/src/main/runtime/anilist-setup-window-main-deps.test.ts b/src/main/runtime/anilist-setup-window-main-deps.test.ts new file mode 100644 index 0000000..eceb34d --- /dev/null +++ b/src/main/runtime/anilist-setup-window-main-deps.test.ts @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildOpenAnilistSetupWindowMainDepsHandler } from './anilist-setup-window-main-deps'; + +test('open anilist setup window main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildOpenAnilistSetupWindowMainDepsHandler({ + maybeFocusExistingSetupWindow: () => false, + createSetupWindow: () => ({}) as never, + buildAuthorizeUrl: () => 'https://anilist.co/auth', + consumeCallbackUrl: () => true, + openSetupInBrowser: (url) => calls.push(`browser:${url}`), + loadManualTokenEntry: () => calls.push('manual'), + redirectUri: 'subminer://anilist-auth', + developerSettingsUrl: 'https://anilist.co/settings/developer', + isAllowedExternalUrl: () => true, + isAllowedNavigationUrl: () => true, + logWarn: (message) => calls.push(`warn:${message}`), + logError: (message) => calls.push(`error:${message}`), + clearSetupWindow: () => calls.push('clear'), + setSetupPageOpened: (opened) => calls.push(`opened:${String(opened)}`), + setSetupWindow: () => calls.push('window'), + openExternal: (url) => calls.push(`external:${url}`), + })(); + + assert.equal(deps.maybeFocusExistingSetupWindow(), false); + assert.equal(deps.buildAuthorizeUrl(), 'https://anilist.co/auth'); + assert.equal(deps.consumeCallbackUrl('subminer://anilist-setup?access_token=x'), true); + assert.equal(deps.redirectUri, 'subminer://anilist-auth'); + assert.equal(deps.developerSettingsUrl, 'https://anilist.co/settings/developer'); + assert.equal(deps.isAllowedExternalUrl('https://anilist.co'), true); + assert.equal(deps.isAllowedNavigationUrl('https://anilist.co/oauth'), true); + deps.openSetupInBrowser('https://anilist.co/auth'); + deps.loadManualTokenEntry({} as never, 'https://anilist.co/auth'); + deps.logWarn('warn'); + deps.logError('error', null); + deps.clearSetupWindow(); + deps.setSetupPageOpened(true); + deps.setSetupWindow({} as never); + deps.openExternal('https://anilist.co'); + + assert.deepEqual(calls, [ + 'browser:https://anilist.co/auth', + 'manual', + 'warn:warn', + 'error:error', + 'clear', + 'opened:true', + 'window', + 'external:https://anilist.co', + ]); +}); diff --git a/src/main/runtime/anilist-setup-window-main-deps.ts b/src/main/runtime/anilist-setup-window-main-deps.ts new file mode 100644 index 0000000..1718d28 --- /dev/null +++ b/src/main/runtime/anilist-setup-window-main-deps.ts @@ -0,0 +1,27 @@ +import type { createOpenAnilistSetupWindowHandler } from './anilist-setup-window'; + +type OpenAnilistSetupWindowMainDeps = Parameters[0]; + +export function createBuildOpenAnilistSetupWindowMainDepsHandler( + deps: OpenAnilistSetupWindowMainDeps, +) { + return (): OpenAnilistSetupWindowMainDeps => ({ + maybeFocusExistingSetupWindow: () => deps.maybeFocusExistingSetupWindow(), + createSetupWindow: () => deps.createSetupWindow(), + buildAuthorizeUrl: () => deps.buildAuthorizeUrl(), + consumeCallbackUrl: (rawUrl: string) => deps.consumeCallbackUrl(rawUrl), + openSetupInBrowser: (authorizeUrl: string) => deps.openSetupInBrowser(authorizeUrl), + loadManualTokenEntry: (setupWindow, authorizeUrl: string) => + deps.loadManualTokenEntry(setupWindow, authorizeUrl), + redirectUri: deps.redirectUri, + developerSettingsUrl: deps.developerSettingsUrl, + isAllowedExternalUrl: (url: string) => deps.isAllowedExternalUrl(url), + isAllowedNavigationUrl: (url: string) => deps.isAllowedNavigationUrl(url), + logWarn: (message: string, details?: unknown) => deps.logWarn(message, details), + logError: (message: string, details: unknown) => deps.logError(message, details), + clearSetupWindow: () => deps.clearSetupWindow(), + setSetupPageOpened: (opened: boolean) => deps.setSetupPageOpened(opened), + setSetupWindow: (setupWindow) => deps.setSetupWindow(setupWindow), + openExternal: (url: string) => deps.openExternal(url), + }); +} diff --git a/src/main/runtime/anilist-token-refresh-main-deps.test.ts b/src/main/runtime/anilist-token-refresh-main-deps.test.ts new file mode 100644 index 0000000..b6b4a72 --- /dev/null +++ b/src/main/runtime/anilist-token-refresh-main-deps.test.ts @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildRefreshAnilistClientSecretStateMainDepsHandler } from './anilist-token-refresh-main-deps'; + +test('refresh anilist client secret state main deps builder maps callbacks', () => { + const calls: string[] = []; + const config = { anilist: { accessToken: 'token' } }; + const deps = createBuildRefreshAnilistClientSecretStateMainDepsHandler({ + getResolvedConfig: () => config as never, + isAnilistTrackingEnabled: () => true, + getCachedAccessToken: () => 'cached', + setCachedAccessToken: () => calls.push('set-cache'), + saveStoredToken: () => calls.push('save'), + loadStoredToken: () => 'stored', + setClientSecretState: () => calls.push('set-state'), + getAnilistSetupPageOpened: () => false, + setAnilistSetupPageOpened: () => calls.push('set-opened'), + openAnilistSetupWindow: () => calls.push('open-window'), + now: () => 123, + })(); + + assert.equal(deps.getResolvedConfig(), config); + assert.equal(deps.isAnilistTrackingEnabled(config as never), true); + assert.equal(deps.getCachedAccessToken(), 'cached'); + deps.setCachedAccessToken(null); + deps.saveStoredToken('x'); + assert.equal(deps.loadStoredToken(), 'stored'); + deps.setClientSecretState({} as never); + assert.equal(deps.getAnilistSetupPageOpened(), false); + deps.setAnilistSetupPageOpened(true); + deps.openAnilistSetupWindow(); + assert.equal(deps.now(), 123); + assert.deepEqual(calls, ['set-cache', 'save', 'set-state', 'set-opened', 'open-window']); +}); diff --git a/src/main/runtime/anilist-token-refresh-main-deps.ts b/src/main/runtime/anilist-token-refresh-main-deps.ts new file mode 100644 index 0000000..40e8dad --- /dev/null +++ b/src/main/runtime/anilist-token-refresh-main-deps.ts @@ -0,0 +1,23 @@ +import type { createRefreshAnilistClientSecretStateHandler } from './anilist-token-refresh'; + +type RefreshAnilistClientSecretStateMainDeps = Parameters< + typeof createRefreshAnilistClientSecretStateHandler +>[0]; + +export function createBuildRefreshAnilistClientSecretStateMainDepsHandler( + deps: RefreshAnilistClientSecretStateMainDeps, +) { + return (): RefreshAnilistClientSecretStateMainDeps => ({ + getResolvedConfig: () => deps.getResolvedConfig(), + isAnilistTrackingEnabled: (config) => deps.isAnilistTrackingEnabled(config), + getCachedAccessToken: () => deps.getCachedAccessToken(), + setCachedAccessToken: (token) => deps.setCachedAccessToken(token), + saveStoredToken: (token: string) => deps.saveStoredToken(token), + loadStoredToken: () => deps.loadStoredToken(), + setClientSecretState: (state) => deps.setClientSecretState(state), + getAnilistSetupPageOpened: () => deps.getAnilistSetupPageOpened(), + setAnilistSetupPageOpened: (opened: boolean) => deps.setAnilistSetupPageOpened(opened), + openAnilistSetupWindow: () => deps.openAnilistSetupWindow(), + now: () => deps.now(), + }); +} diff --git a/src/main/runtime/anki-actions-main-deps.test.ts b/src/main/runtime/anki-actions-main-deps.test.ts new file mode 100644 index 0000000..805508f --- /dev/null +++ b/src/main/runtime/anki-actions-main-deps.test.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildMarkLastCardAsAudioCardMainDepsHandler, + createBuildMineSentenceCardMainDepsHandler, + createBuildRefreshKnownWordCacheMainDepsHandler, + createBuildTriggerFieldGroupingMainDepsHandler, + createBuildUpdateLastCardFromClipboardMainDepsHandler, +} from './anki-actions-main-deps'; + +test('anki action main deps builders map callbacks', async () => { + const calls: string[] = []; + + const update = createBuildUpdateLastCardFromClipboardMainDepsHandler({ + getAnkiIntegration: () => ({ enabled: true }), + readClipboardText: () => 'clip', + showMpvOsd: (text) => calls.push(`osd:${text}`), + updateLastCardFromClipboardCore: async () => { + calls.push('update'); + }, + })(); + assert.deepEqual(update.getAnkiIntegration(), { enabled: true }); + assert.equal(update.readClipboardText(), 'clip'); + update.showMpvOsd('x'); + await update.updateLastCardFromClipboardCore({ + ankiIntegration: { enabled: true }, + readClipboardText: () => '', + showMpvOsd: () => {}, + }); + + const refresh = createBuildRefreshKnownWordCacheMainDepsHandler({ + getAnkiIntegration: () => null, + missingIntegrationMessage: 'missing', + })(); + assert.equal(refresh.getAnkiIntegration(), null); + assert.equal(refresh.missingIntegrationMessage, 'missing'); + + const fieldGrouping = createBuildTriggerFieldGroupingMainDepsHandler({ + getAnkiIntegration: () => ({ enabled: true }), + showMpvOsd: (text) => calls.push(`fg:${text}`), + triggerFieldGroupingCore: async () => { + calls.push('trigger'); + }, + })(); + fieldGrouping.showMpvOsd('fg'); + await fieldGrouping.triggerFieldGroupingCore({ + ankiIntegration: { enabled: true }, + showMpvOsd: () => {}, + }); + + const markAudio = createBuildMarkLastCardAsAudioCardMainDepsHandler({ + getAnkiIntegration: () => ({ enabled: true }), + showMpvOsd: (text) => calls.push(`audio:${text}`), + markLastCardAsAudioCardCore: async () => { + calls.push('mark'); + }, + })(); + markAudio.showMpvOsd('a'); + await markAudio.markLastCardAsAudioCardCore({ + ankiIntegration: { enabled: true }, + showMpvOsd: () => {}, + }); + + const mine = createBuildMineSentenceCardMainDepsHandler({ + getAnkiIntegration: () => ({ enabled: true }), + getMpvClient: () => ({ connected: true }), + showMpvOsd: (text) => calls.push(`mine:${text}`), + mineSentenceCardCore: async () => true, + recordCardsMined: (count) => calls.push(`cards:${count}`), + })(); + assert.deepEqual(mine.getMpvClient(), { connected: true }); + mine.showMpvOsd('m'); + await mine.mineSentenceCardCore({ + ankiIntegration: { enabled: true }, + mpvClient: { connected: true }, + showMpvOsd: () => {}, + }); + mine.recordCardsMined(1); + + assert.deepEqual(calls, [ + 'osd:x', + 'update', + 'fg:fg', + 'trigger', + 'audio:a', + 'mark', + 'mine:m', + 'cards:1', + ]); +}); diff --git a/src/main/runtime/anki-actions-main-deps.ts b/src/main/runtime/anki-actions-main-deps.ts new file mode 100644 index 0000000..8317a57 --- /dev/null +++ b/src/main/runtime/anki-actions-main-deps.ts @@ -0,0 +1,88 @@ +import type { createRefreshKnownWordCacheHandler } from './anki-actions'; + +type RefreshKnownWordCacheMainDeps = Parameters[0]; + +export function createBuildUpdateLastCardFromClipboardMainDepsHandler(deps: { + getAnkiIntegration: () => TAnki; + readClipboardText: () => string; + showMpvOsd: (text: string) => void; + updateLastCardFromClipboardCore: (options: { + ankiIntegration: TAnki; + readClipboardText: () => string; + showMpvOsd: (text: string) => void; + }) => Promise; +}) { + return () => ({ + getAnkiIntegration: () => deps.getAnkiIntegration(), + readClipboardText: () => deps.readClipboardText(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + updateLastCardFromClipboardCore: (options: { + ankiIntegration: TAnki; + readClipboardText: () => string; + showMpvOsd: (text: string) => void; + }) => deps.updateLastCardFromClipboardCore(options), + }); +} + +export function createBuildRefreshKnownWordCacheMainDepsHandler(deps: RefreshKnownWordCacheMainDeps) { + return (): RefreshKnownWordCacheMainDeps => ({ + getAnkiIntegration: () => deps.getAnkiIntegration(), + missingIntegrationMessage: deps.missingIntegrationMessage, + }); +} + +export function createBuildTriggerFieldGroupingMainDepsHandler(deps: { + getAnkiIntegration: () => TAnki; + showMpvOsd: (text: string) => void; + triggerFieldGroupingCore: (options: { + ankiIntegration: TAnki; + showMpvOsd: (text: string) => void; + }) => Promise; +}) { + return () => ({ + getAnkiIntegration: () => deps.getAnkiIntegration(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => + deps.triggerFieldGroupingCore(options), + }); +} + +export function createBuildMarkLastCardAsAudioCardMainDepsHandler(deps: { + getAnkiIntegration: () => TAnki; + showMpvOsd: (text: string) => void; + markLastCardAsAudioCardCore: (options: { + ankiIntegration: TAnki; + showMpvOsd: (text: string) => void; + }) => Promise; +}) { + return () => ({ + getAnkiIntegration: () => deps.getAnkiIntegration(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) => + deps.markLastCardAsAudioCardCore(options), + }); +} + +export function createBuildMineSentenceCardMainDepsHandler(deps: { + getAnkiIntegration: () => TAnki; + getMpvClient: () => TMpv; + showMpvOsd: (text: string) => void; + mineSentenceCardCore: (options: { + ankiIntegration: TAnki; + mpvClient: TMpv; + showMpvOsd: (text: string) => void; + }) => Promise; + recordCardsMined: (count: number) => void; +}) { + return () => ({ + getAnkiIntegration: () => deps.getAnkiIntegration(), + getMpvClient: () => deps.getMpvClient(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + mineSentenceCardCore: (options: { + ankiIntegration: TAnki; + mpvClient: TMpv; + showMpvOsd: (text: string) => void; + }) => deps.mineSentenceCardCore(options), + recordCardsMined: (count: number) => deps.recordCardsMined(count), + }); +} diff --git a/src/main/runtime/field-grouping-resolver-main-deps.test.ts b/src/main/runtime/field-grouping-resolver-main-deps.test.ts new file mode 100644 index 0000000..cdfd334 --- /dev/null +++ b/src/main/runtime/field-grouping-resolver-main-deps.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildGetFieldGroupingResolverMainDepsHandler, + createBuildSetFieldGroupingResolverMainDepsHandler, +} from './field-grouping-resolver-main-deps'; + +test('get field grouping resolver main deps builder maps callbacks', () => { + const resolver = () => undefined; + const deps = createBuildGetFieldGroupingResolverMainDepsHandler({ + getResolver: () => resolver, + })(); + assert.equal(deps.getResolver(), resolver); +}); + +test('set field grouping resolver main deps builder maps callbacks', () => { + const calls: string[] = []; + const wrapped = (choice: unknown) => calls.push(String(choice)); + const deps = createBuildSetFieldGroupingResolverMainDepsHandler({ + setResolver: (resolver) => { + if (resolver) { + resolver('x' as never); + } + }, + nextSequence: () => 2, + getSequence: () => 2, + })(); + + assert.equal(deps.nextSequence(), 2); + assert.equal(deps.getSequence(), 2); + deps.setResolver(wrapped as never); + assert.deepEqual(calls, ['x']); +}); diff --git a/src/main/runtime/field-grouping-resolver-main-deps.ts b/src/main/runtime/field-grouping-resolver-main-deps.ts new file mode 100644 index 0000000..73d54cf --- /dev/null +++ b/src/main/runtime/field-grouping-resolver-main-deps.ts @@ -0,0 +1,25 @@ +import type { + createGetFieldGroupingResolverHandler, + createSetFieldGroupingResolverHandler, +} from './field-grouping-resolver'; + +type GetFieldGroupingResolverMainDeps = Parameters[0]; +type SetFieldGroupingResolverMainDeps = Parameters[0]; + +export function createBuildGetFieldGroupingResolverMainDepsHandler( + deps: GetFieldGroupingResolverMainDeps, +) { + return (): GetFieldGroupingResolverMainDeps => ({ + getResolver: () => deps.getResolver(), + }); +} + +export function createBuildSetFieldGroupingResolverMainDepsHandler( + deps: SetFieldGroupingResolverMainDeps, +) { + return (): SetFieldGroupingResolverMainDeps => ({ + setResolver: (resolver) => deps.setResolver(resolver), + nextSequence: () => deps.nextSequence(), + getSequence: () => deps.getSequence(), + }); +} diff --git a/src/main/runtime/ipc-bridge-actions-main-deps.test.ts b/src/main/runtime/ipc-bridge-actions-main-deps.test.ts new file mode 100644 index 0000000..20615fe --- /dev/null +++ b/src/main/runtime/ipc-bridge-actions-main-deps.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildHandleMpvCommandFromIpcMainDepsHandler, + createBuildRunSubsyncManualFromIpcMainDepsHandler, +} from './ipc-bridge-actions-main-deps'; + +test('ipc bridge action main deps builders map callbacks', async () => { + const calls: string[] = []; + + const handleMpv = createBuildHandleMpvCommandFromIpcMainDepsHandler({ + handleMpvCommandFromIpcRuntime: (command) => calls.push(`mpv:${command.join(':')}`), + buildMpvCommandDeps: () => ({ + triggerSubsyncFromConfig: async () => {}, + openRuntimeOptionsPalette: () => {}, + cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }), + showMpvOsd: () => {}, + replayCurrentSubtitle: () => {}, + playNextSubtitle: () => {}, + sendMpvCommand: () => {}, + isMpvConnected: () => true, + hasRuntimeOptionsManager: () => true, + }), + })(); + handleMpv.handleMpvCommandFromIpcRuntime(['show-text', 'hello'], handleMpv.buildMpvCommandDeps()); + assert.equal(handleMpv.buildMpvCommandDeps().isMpvConnected(), true); + + const runSubsync = createBuildRunSubsyncManualFromIpcMainDepsHandler({ + runManualFromIpc: async (request: { id: string }) => { + calls.push(`subsync:${request.id}`); + return { ok: true as const }; + }, + })(); + assert.deepEqual(await runSubsync.runManualFromIpc({ id: 'job-1' }), { ok: true }); + + assert.deepEqual(calls, ['mpv:show-text:hello', 'subsync:job-1']); +}); diff --git a/src/main/runtime/ipc-bridge-actions-main-deps.ts b/src/main/runtime/ipc-bridge-actions-main-deps.ts new file mode 100644 index 0000000..256455c --- /dev/null +++ b/src/main/runtime/ipc-bridge-actions-main-deps.ts @@ -0,0 +1,21 @@ +import type { createHandleMpvCommandFromIpcHandler } from './ipc-bridge-actions'; + +type HandleMpvCommandFromIpcMainDeps = Parameters[0]; + +export function createBuildHandleMpvCommandFromIpcMainDepsHandler( + deps: HandleMpvCommandFromIpcMainDeps, +) { + return (): HandleMpvCommandFromIpcMainDeps => ({ + handleMpvCommandFromIpcRuntime: (command, options) => + deps.handleMpvCommandFromIpcRuntime(command, options), + buildMpvCommandDeps: () => deps.buildMpvCommandDeps(), + }); +} + +export function createBuildRunSubsyncManualFromIpcMainDepsHandler(deps: { + runManualFromIpc: (request: TRequest) => Promise; +}) { + return () => ({ + runManualFromIpc: (request: TRequest) => deps.runManualFromIpc(request), + }); +} diff --git a/src/main/runtime/ipc-mpv-command-main-deps.test.ts b/src/main/runtime/ipc-mpv-command-main-deps.test.ts new file mode 100644 index 0000000..7a670d6 --- /dev/null +++ b/src/main/runtime/ipc-mpv-command-main-deps.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildMpvCommandFromIpcRuntimeMainDepsHandler } from './ipc-mpv-command-main-deps'; + +test('ipc mpv command main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler({ + triggerSubsyncFromConfig: () => calls.push('subsync'), + openRuntimeOptionsPalette: () => calls.push('palette'), + cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }), + showMpvOsd: (text) => calls.push(`osd:${text}`), + replayCurrentSubtitle: () => calls.push('replay'), + playNextSubtitle: () => calls.push('next'), + sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`), + isMpvConnected: () => true, + hasRuntimeOptionsManager: () => false, + })(); + + deps.triggerSubsyncFromConfig(); + deps.openRuntimeOptionsPalette(); + assert.deepEqual(deps.cycleRuntimeOption('anki.nPlusOneMatchMode', 1), { ok: false, error: 'x' }); + deps.showMpvOsd('hello'); + deps.replayCurrentSubtitle(); + deps.playNextSubtitle(); + deps.sendMpvCommand(['show-text', 'ok']); + assert.equal(deps.isMpvConnected(), true); + assert.equal(deps.hasRuntimeOptionsManager(), false); + assert.deepEqual(calls, [ + 'subsync', + 'palette', + 'osd:hello', + 'replay', + 'next', + 'cmd:show-text:ok', + ]); +}); diff --git a/src/main/runtime/ipc-mpv-command-main-deps.ts b/src/main/runtime/ipc-mpv-command-main-deps.ts new file mode 100644 index 0000000..aed6a2b --- /dev/null +++ b/src/main/runtime/ipc-mpv-command-main-deps.ts @@ -0,0 +1,15 @@ +import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command'; + +export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(deps: MpvCommandFromIpcRuntimeDeps) { + return (): MpvCommandFromIpcRuntimeDeps => ({ + triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), + openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(), + cycleRuntimeOption: (id, direction) => deps.cycleRuntimeOption(id, direction), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + replayCurrentSubtitle: () => deps.replayCurrentSubtitle(), + playNextSubtitle: () => deps.playNextSubtitle(), + sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command), + isMpvConnected: () => deps.isMpvConnected(), + hasRuntimeOptionsManager: () => deps.hasRuntimeOptionsManager(), + }); +} diff --git a/src/main/runtime/jellyfin-remote-session-main-deps.test.ts b/src/main/runtime/jellyfin-remote-session-main-deps.test.ts new file mode 100644 index 0000000..6d5f9b6 --- /dev/null +++ b/src/main/runtime/jellyfin-remote-session-main-deps.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildStartJellyfinRemoteSessionMainDepsHandler, + createBuildStopJellyfinRemoteSessionMainDepsHandler, +} from './jellyfin-remote-session-main-deps'; + +test('start jellyfin remote session main deps builder maps callbacks', async () => { + const calls: string[] = []; + const session = { start: () => {}, stop: () => {}, advertiseNow: async () => true }; + const deps = createBuildStartJellyfinRemoteSessionMainDepsHandler({ + getJellyfinConfig: () => ({ serverUrl: 'http://localhost' }) as never, + getCurrentSession: () => null, + setCurrentSession: () => calls.push('set-session'), + createRemoteSessionService: () => session as never, + defaultDeviceId: 'device', + defaultClientName: 'SubMiner', + defaultClientVersion: '1.0', + handlePlay: async () => { + calls.push('play'); + }, + handlePlaystate: async () => { + calls.push('playstate'); + }, + handleGeneralCommand: async () => { + calls.push('general'); + }, + logInfo: (message) => calls.push(`info:${message}`), + logWarn: (message) => calls.push(`warn:${message}`), + })(); + + assert.deepEqual(deps.getJellyfinConfig(), { serverUrl: 'http://localhost' }); + assert.equal(deps.defaultDeviceId, 'device'); + assert.equal(deps.defaultClientName, 'SubMiner'); + assert.equal(deps.defaultClientVersion, '1.0'); + assert.equal(deps.createRemoteSessionService({} as never), session); + await deps.handlePlay({}); + await deps.handlePlaystate({}); + await deps.handleGeneralCommand({}); + deps.logInfo('connected'); + deps.logWarn('missing'); + assert.deepEqual(calls, ['play', 'playstate', 'general', 'info:connected', 'warn:missing']); +}); + +test('stop jellyfin remote session main deps builder maps callbacks', () => { + const calls: string[] = []; + const session = { start: () => {}, stop: () => {}, advertiseNow: async () => true }; + const deps = createBuildStopJellyfinRemoteSessionMainDepsHandler({ + getCurrentSession: () => session as never, + setCurrentSession: () => calls.push('set-null'), + clearActivePlayback: () => calls.push('clear'), + })(); + + assert.equal(deps.getCurrentSession(), session); + deps.setCurrentSession(null); + deps.clearActivePlayback(); + assert.deepEqual(calls, ['set-null', 'clear']); +}); diff --git a/src/main/runtime/jellyfin-remote-session-main-deps.ts b/src/main/runtime/jellyfin-remote-session-main-deps.ts new file mode 100644 index 0000000..657f583 --- /dev/null +++ b/src/main/runtime/jellyfin-remote-session-main-deps.ts @@ -0,0 +1,36 @@ +import type { + createStartJellyfinRemoteSessionHandler, + createStopJellyfinRemoteSessionHandler, +} from './jellyfin-remote-session-lifecycle'; + +type StartJellyfinRemoteSessionMainDeps = Parameters[0]; +type StopJellyfinRemoteSessionMainDeps = Parameters[0]; + +export function createBuildStartJellyfinRemoteSessionMainDepsHandler( + deps: StartJellyfinRemoteSessionMainDeps, +) { + return (): StartJellyfinRemoteSessionMainDeps => ({ + getJellyfinConfig: () => deps.getJellyfinConfig(), + getCurrentSession: () => deps.getCurrentSession(), + setCurrentSession: (session) => deps.setCurrentSession(session), + createRemoteSessionService: (options) => deps.createRemoteSessionService(options), + defaultDeviceId: deps.defaultDeviceId, + defaultClientName: deps.defaultClientName, + defaultClientVersion: deps.defaultClientVersion, + handlePlay: (payload) => deps.handlePlay(payload), + handlePlaystate: (payload) => deps.handlePlaystate(payload), + handleGeneralCommand: (payload) => deps.handleGeneralCommand(payload), + logInfo: (message: string) => deps.logInfo(message), + logWarn: (message: string, details?: unknown) => deps.logWarn(message, details), + }); +} + +export function createBuildStopJellyfinRemoteSessionMainDepsHandler( + deps: StopJellyfinRemoteSessionMainDeps, +) { + return (): StopJellyfinRemoteSessionMainDeps => ({ + getCurrentSession: () => deps.getCurrentSession(), + setCurrentSession: (session) => deps.setCurrentSession(session), + clearActivePlayback: () => deps.clearActivePlayback(), + }); +} diff --git a/src/main/runtime/jellyfin-setup-window-main-deps.test.ts b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts new file mode 100644 index 0000000..5e0d6f6 --- /dev/null +++ b/src/main/runtime/jellyfin-setup-window-main-deps.test.ts @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildOpenJellyfinSetupWindowMainDepsHandler } from './jellyfin-setup-window-main-deps'; + +test('open jellyfin setup window main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildOpenJellyfinSetupWindowMainDepsHandler({ + maybeFocusExistingSetupWindow: () => false, + createSetupWindow: () => ({}) as never, + getResolvedJellyfinConfig: () => ({ serverUrl: 'http://127.0.0.1:8096', username: 'alice' }), + buildSetupFormHtml: () => '', + parseSubmissionUrl: () => ({ server: 's', username: 'u', password: 'p' }), + authenticateWithPassword: async () => ({ + serverUrl: 'http://127.0.0.1:8096', + username: 'alice', + accessToken: 'token', + userId: 'uid', + }), + getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }), + patchJellyfinConfig: () => calls.push('patch'), + logInfo: (message) => calls.push(`info:${message}`), + logError: (message) => calls.push(`error:${message}`), + showMpvOsd: (message) => calls.push(`osd:${message}`), + clearSetupWindow: () => calls.push('clear'), + setSetupWindow: () => calls.push('set-window'), + encodeURIComponent: (value) => encodeURIComponent(value), + })(); + + assert.equal(deps.maybeFocusExistingSetupWindow(), false); + assert.deepEqual(deps.getResolvedJellyfinConfig(), { + serverUrl: 'http://127.0.0.1:8096', + username: 'alice', + }); + assert.equal(deps.buildSetupFormHtml('a', 'b'), ''); + assert.deepEqual(deps.parseSubmissionUrl('subminer://jellyfin-setup?x=1'), { + server: 's', + username: 'u', + password: 'p', + }); + assert.deepEqual(await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), { + serverUrl: 'http://127.0.0.1:8096', + username: 'alice', + accessToken: 'token', + userId: 'uid', + }); + deps.patchJellyfinConfig({ + serverUrl: 'http://127.0.0.1:8096', + username: 'alice', + accessToken: 'token', + userId: 'uid', + }); + deps.logInfo('ok'); + deps.logError('bad', null); + deps.showMpvOsd('toast'); + deps.clearSetupWindow(); + deps.setSetupWindow({} as never); + assert.equal(deps.encodeURIComponent('a b'), 'a%20b'); + assert.deepEqual(calls, ['patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']); +}); diff --git a/src/main/runtime/jellyfin-setup-window-main-deps.ts b/src/main/runtime/jellyfin-setup-window-main-deps.ts new file mode 100644 index 0000000..cfa0a41 --- /dev/null +++ b/src/main/runtime/jellyfin-setup-window-main-deps.ts @@ -0,0 +1,26 @@ +import type { createOpenJellyfinSetupWindowHandler } from './jellyfin-setup-window'; + +type OpenJellyfinSetupWindowMainDeps = Parameters[0]; + +export function createBuildOpenJellyfinSetupWindowMainDepsHandler( + deps: OpenJellyfinSetupWindowMainDeps, +) { + return (): OpenJellyfinSetupWindowMainDeps => ({ + maybeFocusExistingSetupWindow: () => deps.maybeFocusExistingSetupWindow(), + createSetupWindow: () => deps.createSetupWindow(), + getResolvedJellyfinConfig: () => deps.getResolvedJellyfinConfig(), + buildSetupFormHtml: (defaultServer: string, defaultUser: string) => + deps.buildSetupFormHtml(defaultServer, defaultUser), + parseSubmissionUrl: (rawUrl: string) => deps.parseSubmissionUrl(rawUrl), + authenticateWithPassword: (server: string, username: string, password: string, clientInfo) => + deps.authenticateWithPassword(server, username, password, clientInfo), + getJellyfinClientInfo: () => deps.getJellyfinClientInfo(), + patchJellyfinConfig: (session) => deps.patchJellyfinConfig(session), + logInfo: (message: string) => deps.logInfo(message), + logError: (message: string, error: unknown) => deps.logError(message, error), + showMpvOsd: (message: string) => deps.showMpvOsd(message), + clearSetupWindow: () => deps.clearSetupWindow(), + setSetupWindow: (window) => deps.setSetupWindow(window), + encodeURIComponent: (value: string) => deps.encodeURIComponent(value), + }); +} diff --git a/src/main/runtime/jellyfin-subtitle-preload-main-deps.test.ts b/src/main/runtime/jellyfin-subtitle-preload-main-deps.test.ts new file mode 100644 index 0000000..ce6f8d9 --- /dev/null +++ b/src/main/runtime/jellyfin-subtitle-preload-main-deps.test.ts @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler } from './jellyfin-subtitle-preload-main-deps'; + +test('preload jellyfin external subtitles main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler({ + listJellyfinSubtitleTracks: async () => { + calls.push('list'); + return []; + }, + getMpvClient: () => ({ requestProperty: async () => [] }), + sendMpvCommand: () => calls.push('send'), + wait: async () => { + calls.push('wait'); + }, + logDebug: (message) => calls.push(`debug:${message}`), + })(); + + await deps.listJellyfinSubtitleTracks({} as never, {} as never, 'item'); + assert.equal(typeof deps.getMpvClient()?.requestProperty, 'function'); + deps.sendMpvCommand(['set_property', 'sid', 'auto']); + await deps.wait(1); + deps.logDebug('oops', null); + assert.deepEqual(calls, ['list', 'send', 'wait', 'debug:oops']); +}); diff --git a/src/main/runtime/jellyfin-subtitle-preload-main-deps.ts b/src/main/runtime/jellyfin-subtitle-preload-main-deps.ts new file mode 100644 index 0000000..ed84df5 --- /dev/null +++ b/src/main/runtime/jellyfin-subtitle-preload-main-deps.ts @@ -0,0 +1,18 @@ +import type { createPreloadJellyfinExternalSubtitlesHandler } from './jellyfin-subtitle-preload'; + +type PreloadJellyfinExternalSubtitlesMainDeps = Parameters< + typeof createPreloadJellyfinExternalSubtitlesHandler +>[0]; + +export function createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler( + deps: PreloadJellyfinExternalSubtitlesMainDeps, +) { + return (): PreloadJellyfinExternalSubtitlesMainDeps => ({ + listJellyfinSubtitleTracks: (session, clientInfo, itemId) => + deps.listJellyfinSubtitleTracks(session, clientInfo, itemId), + getMpvClient: () => deps.getMpvClient(), + sendMpvCommand: (command) => deps.sendMpvCommand(command), + wait: (ms: number) => deps.wait(ms), + logDebug: (message: string, error: unknown) => deps.logDebug(message, error), + }); +} diff --git a/src/main/runtime/mining-actions-main-deps.test.ts b/src/main/runtime/mining-actions-main-deps.test.ts new file mode 100644 index 0000000..389c3f5 --- /dev/null +++ b/src/main/runtime/mining-actions-main-deps.test.ts @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildCopyCurrentSubtitleMainDepsHandler, + createBuildHandleMineSentenceDigitMainDepsHandler, + createBuildHandleMultiCopyDigitMainDepsHandler, +} from './mining-actions-main-deps'; + +test('mining action main deps builders map callbacks', () => { + const calls: string[] = []; + + const multiCopy = createBuildHandleMultiCopyDigitMainDepsHandler({ + getSubtitleTimingTracker: () => ({ track: true }), + writeClipboardText: (text) => calls.push(`clip:${text}`), + showMpvOsd: (text) => calls.push(`osd:${text}`), + handleMultiCopyDigitCore: () => calls.push('multi-copy'), + })(); + assert.deepEqual(multiCopy.getSubtitleTimingTracker(), { track: true }); + multiCopy.writeClipboardText('x'); + multiCopy.showMpvOsd('y'); + multiCopy.handleMultiCopyDigitCore(2, { + subtitleTimingTracker: { track: true }, + writeClipboardText: () => {}, + showMpvOsd: () => {}, + }); + + const copyCurrent = createBuildCopyCurrentSubtitleMainDepsHandler({ + getSubtitleTimingTracker: () => ({ track: true }), + writeClipboardText: (text) => calls.push(`copy:${text}`), + showMpvOsd: (text) => calls.push(`copy-osd:${text}`), + copyCurrentSubtitleCore: () => calls.push('copy-current'), + })(); + assert.deepEqual(copyCurrent.getSubtitleTimingTracker(), { track: true }); + copyCurrent.writeClipboardText('a'); + copyCurrent.showMpvOsd('b'); + copyCurrent.copyCurrentSubtitleCore({ + subtitleTimingTracker: { track: true }, + writeClipboardText: () => {}, + showMpvOsd: () => {}, + }); + + const mineDigit = createBuildHandleMineSentenceDigitMainDepsHandler({ + getSubtitleTimingTracker: () => ({ track: true }), + getAnkiIntegration: () => ({ enabled: true }), + getCurrentSecondarySubText: () => 'sub', + showMpvOsd: (text) => calls.push(`mine-osd:${text}`), + logError: (message) => calls.push(`err:${message}`), + onCardsMined: (count) => calls.push(`cards:${count}`), + handleMineSentenceDigitCore: () => calls.push('mine-digit'), + })(); + assert.equal(mineDigit.getCurrentSecondarySubText(), 'sub'); + mineDigit.showMpvOsd('done'); + mineDigit.logError('bad', null); + mineDigit.onCardsMined(2); + mineDigit.handleMineSentenceDigitCore(2, { + subtitleTimingTracker: { track: true }, + ankiIntegration: { enabled: true }, + getCurrentSecondarySubText: () => 'sub', + showMpvOsd: () => {}, + logError: () => {}, + onCardsMined: () => {}, + }); + + assert.deepEqual(calls, [ + 'clip:x', + 'osd:y', + 'multi-copy', + 'copy:a', + 'copy-osd:b', + 'copy-current', + 'mine-osd:done', + 'err:bad', + 'cards:2', + 'mine-digit', + ]); +}); diff --git a/src/main/runtime/mining-actions-main-deps.ts b/src/main/runtime/mining-actions-main-deps.ts new file mode 100644 index 0000000..e79fdbd --- /dev/null +++ b/src/main/runtime/mining-actions-main-deps.ts @@ -0,0 +1,92 @@ +export function createBuildHandleMultiCopyDigitMainDepsHandler(deps: { + getSubtitleTimingTracker: () => TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + handleMultiCopyDigitCore: ( + count: number, + options: { + subtitleTimingTracker: TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + }, + ) => void; +}) { + return () => ({ + getSubtitleTimingTracker: () => deps.getSubtitleTimingTracker(), + writeClipboardText: (text: string) => deps.writeClipboardText(text), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + handleMultiCopyDigitCore: ( + count: number, + options: { + subtitleTimingTracker: TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + }, + ) => deps.handleMultiCopyDigitCore(count, options), + }); +} + +export function createBuildCopyCurrentSubtitleMainDepsHandler(deps: { + getSubtitleTimingTracker: () => TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + copyCurrentSubtitleCore: (options: { + subtitleTimingTracker: TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + }) => void; +}) { + return () => ({ + getSubtitleTimingTracker: () => deps.getSubtitleTimingTracker(), + writeClipboardText: (text: string) => deps.writeClipboardText(text), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + copyCurrentSubtitleCore: (options: { + subtitleTimingTracker: TSubtitleTimingTracker; + writeClipboardText: (text: string) => void; + showMpvOsd: (text: string) => void; + }) => deps.copyCurrentSubtitleCore(options), + }); +} + +export function createBuildHandleMineSentenceDigitMainDepsHandler< + TSubtitleTimingTracker, + TAnkiIntegration, +>(deps: { + getSubtitleTimingTracker: () => TSubtitleTimingTracker; + getAnkiIntegration: () => TAnkiIntegration; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + onCardsMined: (count: number) => void; + handleMineSentenceDigitCore: ( + count: number, + options: { + subtitleTimingTracker: TSubtitleTimingTracker; + ankiIntegration: TAnkiIntegration; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + onCardsMined: (count: number) => void; + }, + ) => void; +}) { + return () => ({ + getSubtitleTimingTracker: () => deps.getSubtitleTimingTracker(), + getAnkiIntegration: () => deps.getAnkiIntegration(), + getCurrentSecondarySubText: () => deps.getCurrentSecondarySubText(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + logError: (message: string, err: unknown) => deps.logError(message, err), + onCardsMined: (count: number) => deps.onCardsMined(count), + handleMineSentenceDigitCore: ( + count: number, + options: { + subtitleTimingTracker: TSubtitleTimingTracker; + ankiIntegration: TAnkiIntegration; + getCurrentSecondarySubText: () => string | undefined; + showMpvOsd: (text: string) => void; + logError: (message: string, err: unknown) => void; + onCardsMined: (count: number) => void; + }, + ) => deps.handleMineSentenceDigitCore(count, options), + }); +} diff --git a/src/main/runtime/numeric-shortcut-session-main-deps.test.ts b/src/main/runtime/numeric-shortcut-session-main-deps.test.ts new file mode 100644 index 0000000..ae0242e --- /dev/null +++ b/src/main/runtime/numeric-shortcut-session-main-deps.test.ts @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildCancelNumericShortcutSessionMainDepsHandler, + createBuildStartNumericShortcutSessionMainDepsHandler, +} from './numeric-shortcut-session-main-deps'; + +test('numeric shortcut session main deps builders map callbacks', () => { + const calls: string[] = []; + const session = { + start: () => calls.push('start'), + cancel: () => calls.push('cancel'), + }; + + const cancel = createBuildCancelNumericShortcutSessionMainDepsHandler({ session })(); + cancel.session.cancel(); + + const start = createBuildStartNumericShortcutSessionMainDepsHandler({ + session, + onDigit: (digit) => calls.push(`digit:${digit}`), + messages: { + prompt: 'prompt', + timeout: 'timeout', + cancelled: 'cancelled', + }, + })(); + start.session.start({ + timeoutMs: 100, + onDigit: () => {}, + messages: start.messages, + }); + start.onDigit(4); + assert.equal(start.messages.prompt, 'prompt'); + assert.equal(start.messages.timeout, 'timeout'); + assert.equal(start.messages.cancelled, 'cancelled'); + + assert.deepEqual(calls, ['cancel', 'start', 'digit:4']); +}); diff --git a/src/main/runtime/numeric-shortcut-session-main-deps.ts b/src/main/runtime/numeric-shortcut-session-main-deps.ts new file mode 100644 index 0000000..1970d9c --- /dev/null +++ b/src/main/runtime/numeric-shortcut-session-main-deps.ts @@ -0,0 +1,25 @@ +import type { + createCancelNumericShortcutSessionHandler, + createStartNumericShortcutSessionHandler, +} from './numeric-shortcut-session-handlers'; + +type CancelNumericShortcutSessionMainDeps = Parameters[0]; +type StartNumericShortcutSessionMainDeps = Parameters[0]; + +export function createBuildCancelNumericShortcutSessionMainDepsHandler( + deps: CancelNumericShortcutSessionMainDeps, +) { + return (): CancelNumericShortcutSessionMainDeps => ({ + session: deps.session, + }); +} + +export function createBuildStartNumericShortcutSessionMainDepsHandler( + deps: StartNumericShortcutSessionMainDeps, +) { + return (): StartNumericShortcutSessionMainDeps => ({ + session: deps.session, + onDigit: (digit: number) => deps.onDigit(digit), + messages: deps.messages, + }); +} diff --git a/src/main/runtime/overlay-main-actions-main-deps.test.ts b/src/main/runtime/overlay-main-actions-main-deps.test.ts new file mode 100644 index 0000000..c9731fa --- /dev/null +++ b/src/main/runtime/overlay-main-actions-main-deps.test.ts @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildAppendClipboardVideoToQueueMainDepsHandler, + createBuildHandleOverlayModalClosedMainDepsHandler, + createBuildSetOverlayVisibleMainDepsHandler, + createBuildToggleOverlayMainDepsHandler, +} from './overlay-main-actions-main-deps'; + +test('overlay main action main deps builders map callbacks', () => { + const calls: string[] = []; + + const setOverlay = createBuildSetOverlayVisibleMainDepsHandler({ + setVisibleOverlayVisible: (visible) => calls.push(`set:${visible}`), + })(); + setOverlay.setVisibleOverlayVisible(true); + + const toggleOverlay = createBuildToggleOverlayMainDepsHandler({ + toggleVisibleOverlay: () => calls.push('toggle'), + })(); + toggleOverlay.toggleVisibleOverlay(); + + const modalClosed = createBuildHandleOverlayModalClosedMainDepsHandler({ + handleOverlayModalClosedRuntime: (modal) => calls.push(`modal:${modal}`), + })(); + modalClosed.handleOverlayModalClosedRuntime('runtime-options'); + + const append = createBuildAppendClipboardVideoToQueueMainDepsHandler({ + appendClipboardVideoToQueueRuntime: () => { + calls.push('append'); + return { ok: true, message: 'ok' }; + }, + getMpvClient: () => ({ connected: true }), + readClipboardText: () => '/tmp/v.mkv', + showMpvOsd: (text) => calls.push(`osd:${text}`), + sendMpvCommand: (command) => calls.push(`cmd:${command.join(':')}`), + })(); + assert.deepEqual(append.appendClipboardVideoToQueueRuntime({ + getMpvClient: () => ({ connected: true }), + readClipboardText: () => '/tmp/v.mkv', + showMpvOsd: () => {}, + sendMpvCommand: () => {}, + }), { ok: true, message: 'ok' }); + assert.equal(append.readClipboardText(), '/tmp/v.mkv'); + assert.equal(typeof append.getMpvClient(), 'object'); + append.showMpvOsd('queued'); + append.sendMpvCommand(['loadfile', '/tmp/v.mkv', 'append']); + + assert.deepEqual(calls, [ + 'set:true', + 'toggle', + 'modal:runtime-options', + 'append', + 'osd:queued', + 'cmd:loadfile:/tmp/v.mkv:append', + ]); +}); diff --git a/src/main/runtime/overlay-main-actions-main-deps.ts b/src/main/runtime/overlay-main-actions-main-deps.ts new file mode 100644 index 0000000..387c6d4 --- /dev/null +++ b/src/main/runtime/overlay-main-actions-main-deps.ts @@ -0,0 +1,43 @@ +import type { + createAppendClipboardVideoToQueueHandler, + createHandleOverlayModalClosedHandler, + createSetOverlayVisibleHandler, + createToggleOverlayHandler, +} from './overlay-main-actions'; + +type SetOverlayVisibleMainDeps = Parameters[0]; +type ToggleOverlayMainDeps = Parameters[0]; +type HandleOverlayModalClosedMainDeps = Parameters[0]; +type AppendClipboardVideoToQueueMainDeps = Parameters[0]; + +export function createBuildSetOverlayVisibleMainDepsHandler(deps: SetOverlayVisibleMainDeps) { + return (): SetOverlayVisibleMainDeps => ({ + setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), + }); +} + +export function createBuildToggleOverlayMainDepsHandler(deps: ToggleOverlayMainDeps) { + return (): ToggleOverlayMainDeps => ({ + toggleVisibleOverlay: () => deps.toggleVisibleOverlay(), + }); +} + +export function createBuildHandleOverlayModalClosedMainDepsHandler( + deps: HandleOverlayModalClosedMainDeps, +) { + return (): HandleOverlayModalClosedMainDeps => ({ + handleOverlayModalClosedRuntime: (modal) => deps.handleOverlayModalClosedRuntime(modal), + }); +} + +export function createBuildAppendClipboardVideoToQueueMainDepsHandler( + deps: AppendClipboardVideoToQueueMainDeps, +) { + return (): AppendClipboardVideoToQueueMainDeps => ({ + appendClipboardVideoToQueueRuntime: (options) => deps.appendClipboardVideoToQueueRuntime(options), + getMpvClient: () => deps.getMpvClient(), + readClipboardText: () => deps.readClipboardText(), + showMpvOsd: (text: string) => deps.showMpvOsd(text), + sendMpvCommand: (command: (string | number)[]) => deps.sendMpvCommand(command), + }); +} diff --git a/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.test.ts b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.test.ts new file mode 100644 index 0000000..f5c7e37 --- /dev/null +++ b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildRefreshOverlayShortcutsMainDepsHandler, + createBuildRegisterOverlayShortcutsMainDepsHandler, + createBuildSyncOverlayShortcutsMainDepsHandler, + createBuildUnregisterOverlayShortcutsMainDepsHandler, +} from './overlay-shortcuts-lifecycle-main-deps'; + +test('overlay shortcuts lifecycle main deps builders map runtime instance', () => { + const runtime = { + registerOverlayShortcuts: () => {}, + unregisterOverlayShortcuts: () => {}, + syncOverlayShortcuts: () => {}, + refreshOverlayShortcuts: () => {}, + }; + + const register = createBuildRegisterOverlayShortcutsMainDepsHandler({ + overlayShortcutsRuntime: runtime, + })(); + const unregister = createBuildUnregisterOverlayShortcutsMainDepsHandler({ + overlayShortcutsRuntime: runtime, + })(); + const sync = createBuildSyncOverlayShortcutsMainDepsHandler({ + overlayShortcutsRuntime: runtime, + })(); + const refresh = createBuildRefreshOverlayShortcutsMainDepsHandler({ + overlayShortcutsRuntime: runtime, + })(); + + assert.equal(register.overlayShortcutsRuntime, runtime); + assert.equal(unregister.overlayShortcutsRuntime, runtime); + assert.equal(sync.overlayShortcutsRuntime, runtime); + assert.equal(refresh.overlayShortcutsRuntime, runtime); +}); diff --git a/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts new file mode 100644 index 0000000..25814ac --- /dev/null +++ b/src/main/runtime/overlay-shortcuts-lifecycle-main-deps.ts @@ -0,0 +1,41 @@ +import type { + createRefreshOverlayShortcutsHandler, + createRegisterOverlayShortcutsHandler, + createSyncOverlayShortcutsHandler, + createUnregisterOverlayShortcutsHandler, +} from './overlay-shortcuts-lifecycle'; + +type RegisterOverlayShortcutsMainDeps = Parameters[0]; +type UnregisterOverlayShortcutsMainDeps = Parameters[0]; +type SyncOverlayShortcutsMainDeps = Parameters[0]; +type RefreshOverlayShortcutsMainDeps = Parameters[0]; + +export function createBuildRegisterOverlayShortcutsMainDepsHandler( + deps: RegisterOverlayShortcutsMainDeps, +) { + return (): RegisterOverlayShortcutsMainDeps => ({ + overlayShortcutsRuntime: deps.overlayShortcutsRuntime, + }); +} + +export function createBuildUnregisterOverlayShortcutsMainDepsHandler( + deps: UnregisterOverlayShortcutsMainDeps, +) { + return (): UnregisterOverlayShortcutsMainDeps => ({ + overlayShortcutsRuntime: deps.overlayShortcutsRuntime, + }); +} + +export function createBuildSyncOverlayShortcutsMainDepsHandler(deps: SyncOverlayShortcutsMainDeps) { + return (): SyncOverlayShortcutsMainDeps => ({ + overlayShortcutsRuntime: deps.overlayShortcutsRuntime, + }); +} + +export function createBuildRefreshOverlayShortcutsMainDepsHandler( + deps: RefreshOverlayShortcutsMainDeps, +) { + return (): RefreshOverlayShortcutsMainDeps => ({ + overlayShortcutsRuntime: deps.overlayShortcutsRuntime, + }); +} diff --git a/src/main/runtime/overlay-visibility-actions-main-deps.test.ts b/src/main/runtime/overlay-visibility-actions-main-deps.test.ts new file mode 100644 index 0000000..c2f5f05 --- /dev/null +++ b/src/main/runtime/overlay-visibility-actions-main-deps.test.ts @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildSetInvisibleOverlayVisibleMainDepsHandler, + createBuildSetVisibleOverlayVisibleMainDepsHandler, + createBuildToggleInvisibleOverlayMainDepsHandler, + createBuildToggleVisibleOverlayMainDepsHandler, +} from './overlay-visibility-actions-main-deps'; + +test('overlay visibility action main deps builders map callbacks', () => { + const calls: string[] = []; + + const setVisible = createBuildSetVisibleOverlayVisibleMainDepsHandler({ + setVisibleOverlayVisibleCore: () => calls.push('visible-core'), + setVisibleOverlayVisibleState: (visible) => calls.push(`visible-state:${visible}`), + updateVisibleOverlayVisibility: () => calls.push('update-visible'), + updateInvisibleOverlayVisibility: () => calls.push('update-invisible'), + syncInvisibleOverlayMousePassthrough: () => calls.push('sync'), + shouldBindVisibleOverlayToMpvSubVisibility: () => true, + isMpvConnected: () => true, + setMpvSubVisibility: (visible) => calls.push(`mpv:${visible}`), + })(); + setVisible.setVisibleOverlayVisibleCore({ + visible: true, + setVisibleOverlayVisibleState: () => {}, + updateVisibleOverlayVisibility: () => {}, + updateInvisibleOverlayVisibility: () => {}, + syncInvisibleOverlayMousePassthrough: () => {}, + shouldBindVisibleOverlayToMpvSubVisibility: () => true, + isMpvConnected: () => true, + setMpvSubVisibility: () => {}, + }); + setVisible.setVisibleOverlayVisibleState(true); + setVisible.updateVisibleOverlayVisibility(); + setVisible.updateInvisibleOverlayVisibility(); + setVisible.syncInvisibleOverlayMousePassthrough(); + assert.equal(setVisible.shouldBindVisibleOverlayToMpvSubVisibility(), true); + assert.equal(setVisible.isMpvConnected(), true); + setVisible.setMpvSubVisibility(false); + + const setInvisible = createBuildSetInvisibleOverlayVisibleMainDepsHandler({ + setInvisibleOverlayVisibleCore: () => calls.push('invisible-core'), + setInvisibleOverlayVisibleState: (visible) => calls.push(`invisible-state:${visible}`), + updateInvisibleOverlayVisibility: () => calls.push('update-only-invisible'), + syncInvisibleOverlayMousePassthrough: () => calls.push('sync-only'), + })(); + setInvisible.setInvisibleOverlayVisibleCore({ + visible: false, + setInvisibleOverlayVisibleState: () => {}, + updateInvisibleOverlayVisibility: () => {}, + syncInvisibleOverlayMousePassthrough: () => {}, + }); + setInvisible.setInvisibleOverlayVisibleState(false); + setInvisible.updateInvisibleOverlayVisibility(); + setInvisible.syncInvisibleOverlayMousePassthrough(); + + const toggleVisible = createBuildToggleVisibleOverlayMainDepsHandler({ + getVisibleOverlayVisible: () => false, + setVisibleOverlayVisible: (visible) => calls.push(`toggle-visible:${visible}`), + })(); + assert.equal(toggleVisible.getVisibleOverlayVisible(), false); + toggleVisible.setVisibleOverlayVisible(true); + + const toggleInvisible = createBuildToggleInvisibleOverlayMainDepsHandler({ + getInvisibleOverlayVisible: () => true, + setInvisibleOverlayVisible: (visible) => calls.push(`toggle-invisible:${visible}`), + })(); + assert.equal(toggleInvisible.getInvisibleOverlayVisible(), true); + toggleInvisible.setInvisibleOverlayVisible(false); + + assert.deepEqual(calls, [ + 'visible-core', + 'visible-state:true', + 'update-visible', + 'update-invisible', + 'sync', + 'mpv:false', + 'invisible-core', + 'invisible-state:false', + 'update-only-invisible', + 'sync-only', + 'toggle-visible:true', + 'toggle-invisible:false', + ]); +}); diff --git a/src/main/runtime/overlay-visibility-actions-main-deps.ts b/src/main/runtime/overlay-visibility-actions-main-deps.ts new file mode 100644 index 0000000..f4b9941 --- /dev/null +++ b/src/main/runtime/overlay-visibility-actions-main-deps.ts @@ -0,0 +1,53 @@ +import type { + createSetInvisibleOverlayVisibleHandler, + createSetVisibleOverlayVisibleHandler, + createToggleInvisibleOverlayHandler, + createToggleVisibleOverlayHandler, +} from './overlay-visibility-actions'; + +type SetVisibleOverlayVisibleMainDeps = Parameters[0]; +type SetInvisibleOverlayVisibleMainDeps = Parameters[0]; +type ToggleVisibleOverlayMainDeps = Parameters[0]; +type ToggleInvisibleOverlayMainDeps = Parameters[0]; + +export function createBuildSetVisibleOverlayVisibleMainDepsHandler( + deps: SetVisibleOverlayVisibleMainDeps, +) { + return (): SetVisibleOverlayVisibleMainDeps => ({ + setVisibleOverlayVisibleCore: (options) => deps.setVisibleOverlayVisibleCore(options), + setVisibleOverlayVisibleState: (visible: boolean) => deps.setVisibleOverlayVisibleState(visible), + updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(), + updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), + syncInvisibleOverlayMousePassthrough: () => deps.syncInvisibleOverlayMousePassthrough(), + shouldBindVisibleOverlayToMpvSubVisibility: () => deps.shouldBindVisibleOverlayToMpvSubVisibility(), + isMpvConnected: () => deps.isMpvConnected(), + setMpvSubVisibility: (visible: boolean) => deps.setMpvSubVisibility(visible), + }); +} + +export function createBuildSetInvisibleOverlayVisibleMainDepsHandler( + deps: SetInvisibleOverlayVisibleMainDeps, +) { + return (): SetInvisibleOverlayVisibleMainDeps => ({ + setInvisibleOverlayVisibleCore: (options) => deps.setInvisibleOverlayVisibleCore(options), + setInvisibleOverlayVisibleState: (visible: boolean) => deps.setInvisibleOverlayVisibleState(visible), + updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(), + syncInvisibleOverlayMousePassthrough: () => deps.syncInvisibleOverlayMousePassthrough(), + }); +} + +export function createBuildToggleVisibleOverlayMainDepsHandler(deps: ToggleVisibleOverlayMainDeps) { + return (): ToggleVisibleOverlayMainDeps => ({ + getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), + setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), + }); +} + +export function createBuildToggleInvisibleOverlayMainDepsHandler( + deps: ToggleInvisibleOverlayMainDeps, +) { + return (): ToggleInvisibleOverlayMainDeps => ({ + getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), + setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), + }); +} diff --git a/src/main/runtime/overlay-window-layout-main-deps.test.ts b/src/main/runtime/overlay-window-layout-main-deps.test.ts new file mode 100644 index 0000000..4ced712 --- /dev/null +++ b/src/main/runtime/overlay-window-layout-main-deps.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildEnforceOverlayLayerOrderMainDepsHandler, + createBuildEnsureOverlayWindowLevelMainDepsHandler, + createBuildUpdateInvisibleOverlayBoundsMainDepsHandler, + createBuildUpdateVisibleOverlayBoundsMainDepsHandler, +} from './overlay-window-layout-main-deps'; + +test('overlay window layout main deps builders map callbacks', () => { + const calls: string[] = []; + + const visible = createBuildUpdateVisibleOverlayBoundsMainDepsHandler({ + setOverlayWindowBounds: (layer) => calls.push(`visible:${layer}`), + })(); + visible.setOverlayWindowBounds('visible', { x: 0, y: 0, width: 1, height: 1 }); + + const invisible = createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({ + setOverlayWindowBounds: (layer) => calls.push(`invisible:${layer}`), + })(); + invisible.setOverlayWindowBounds('invisible', { x: 0, y: 0, width: 1, height: 1 }); + + const level = createBuildEnsureOverlayWindowLevelMainDepsHandler({ + ensureOverlayWindowLevelCore: () => calls.push('ensure'), + })(); + level.ensureOverlayWindowLevelCore({}); + + const order = createBuildEnforceOverlayLayerOrderMainDepsHandler({ + enforceOverlayLayerOrderCore: () => calls.push('order'), + getVisibleOverlayVisible: () => true, + getInvisibleOverlayVisible: () => false, + getMainWindow: () => ({ kind: 'main' }), + getInvisibleWindow: () => ({ kind: 'invisible' }), + ensureOverlayWindowLevel: () => calls.push('order-level'), + })(); + order.enforceOverlayLayerOrderCore({ + visibleOverlayVisible: true, + invisibleOverlayVisible: false, + mainWindow: null, + invisibleWindow: null, + ensureOverlayWindowLevel: () => {}, + }); + assert.equal(order.getVisibleOverlayVisible(), true); + assert.equal(order.getInvisibleOverlayVisible(), false); + assert.deepEqual(order.getMainWindow(), { kind: 'main' }); + assert.deepEqual(order.getInvisibleWindow(), { kind: 'invisible' }); + order.ensureOverlayWindowLevel({}); + + assert.deepEqual(calls, [ + 'visible:visible', + 'invisible:invisible', + 'ensure', + 'order', + 'order-level', + ]); +}); diff --git a/src/main/runtime/overlay-window-layout-main-deps.ts b/src/main/runtime/overlay-window-layout-main-deps.ts new file mode 100644 index 0000000..317cacd --- /dev/null +++ b/src/main/runtime/overlay-window-layout-main-deps.ts @@ -0,0 +1,48 @@ +import type { + createEnforceOverlayLayerOrderHandler, + createEnsureOverlayWindowLevelHandler, + createUpdateInvisibleOverlayBoundsHandler, + createUpdateVisibleOverlayBoundsHandler, +} from './overlay-window-layout'; + +type UpdateVisibleOverlayBoundsMainDeps = Parameters[0]; +type UpdateInvisibleOverlayBoundsMainDeps = Parameters[0]; +type EnsureOverlayWindowLevelMainDeps = Parameters[0]; +type EnforceOverlayLayerOrderMainDeps = Parameters[0]; + +export function createBuildUpdateVisibleOverlayBoundsMainDepsHandler( + deps: UpdateVisibleOverlayBoundsMainDeps, +) { + return (): UpdateVisibleOverlayBoundsMainDeps => ({ + setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry), + }); +} + +export function createBuildUpdateInvisibleOverlayBoundsMainDepsHandler( + deps: UpdateInvisibleOverlayBoundsMainDeps, +) { + return (): UpdateInvisibleOverlayBoundsMainDeps => ({ + setOverlayWindowBounds: (layer, geometry) => deps.setOverlayWindowBounds(layer, geometry), + }); +} + +export function createBuildEnsureOverlayWindowLevelMainDepsHandler( + deps: EnsureOverlayWindowLevelMainDeps, +) { + return (): EnsureOverlayWindowLevelMainDeps => ({ + ensureOverlayWindowLevelCore: (window: unknown) => deps.ensureOverlayWindowLevelCore(window), + }); +} + +export function createBuildEnforceOverlayLayerOrderMainDepsHandler( + deps: EnforceOverlayLayerOrderMainDeps, +) { + return (): EnforceOverlayLayerOrderMainDeps => ({ + enforceOverlayLayerOrderCore: (params) => deps.enforceOverlayLayerOrderCore(params), + getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), + getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), + getMainWindow: () => deps.getMainWindow(), + getInvisibleWindow: () => deps.getInvisibleWindow(), + ensureOverlayWindowLevel: (window: unknown) => deps.ensureOverlayWindowLevel(window), + }); +} diff --git a/src/main/runtime/startup-warmups-main-deps.test.ts b/src/main/runtime/startup-warmups-main-deps.test.ts new file mode 100644 index 0000000..aca4ba8 --- /dev/null +++ b/src/main/runtime/startup-warmups-main-deps.test.ts @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildLaunchBackgroundWarmupTaskMainDepsHandler, + createBuildStartBackgroundWarmupsMainDepsHandler, +} from './startup-warmups-main-deps'; + +test('startup warmups main deps builders map callbacks', async () => { + const calls: string[] = []; + + const launch = createBuildLaunchBackgroundWarmupTaskMainDepsHandler({ + now: () => 11, + logDebug: (message) => calls.push(`debug:${message}`), + logWarn: (message) => calls.push(`warn:${message}`), + })(); + assert.equal(launch.now(), 11); + launch.logDebug('x'); + launch.logWarn('y'); + + const start = createBuildStartBackgroundWarmupsMainDepsHandler({ + getStarted: () => false, + setStarted: (started) => calls.push(`started:${started}`), + isTexthookerOnlyMode: () => false, + launchTask: (label, task) => { + calls.push(`launch:${label}`); + void task(); + }, + createMecabTokenizerAndCheck: async () => { + calls.push('mecab'); + }, + ensureYomitanExtensionLoaded: async () => { + calls.push('yomitan'); + }, + prewarmSubtitleDictionaries: async () => { + calls.push('dict'); + }, + shouldAutoConnectJellyfinRemote: () => true, + startJellyfinRemoteSession: async () => { + calls.push('jellyfin'); + }, + })(); + assert.equal(start.getStarted(), false); + start.setStarted(true); + assert.equal(start.isTexthookerOnlyMode(), false); + start.launchTask('demo', async () => { + calls.push('task'); + }); + await start.createMecabTokenizerAndCheck(); + await start.ensureYomitanExtensionLoaded(); + await start.prewarmSubtitleDictionaries(); + assert.equal(start.shouldAutoConnectJellyfinRemote(), true); + await start.startJellyfinRemoteSession(); + + assert.deepEqual(calls, [ + 'debug:x', + 'warn:y', + 'started:true', + 'launch:demo', + 'task', + 'mecab', + 'yomitan', + 'dict', + 'jellyfin', + ]); +}); diff --git a/src/main/runtime/startup-warmups-main-deps.ts b/src/main/runtime/startup-warmups-main-deps.ts new file mode 100644 index 0000000..23795a4 --- /dev/null +++ b/src/main/runtime/startup-warmups-main-deps.ts @@ -0,0 +1,31 @@ +import type { + createLaunchBackgroundWarmupTaskHandler, + createStartBackgroundWarmupsHandler, +} from './startup-warmups'; + +type LaunchBackgroundWarmupTaskMainDeps = Parameters[0]; +type StartBackgroundWarmupsMainDeps = Parameters[0]; + +export function createBuildLaunchBackgroundWarmupTaskMainDepsHandler( + deps: LaunchBackgroundWarmupTaskMainDeps, +) { + return (): LaunchBackgroundWarmupTaskMainDeps => ({ + now: () => deps.now(), + logDebug: (message: string) => deps.logDebug(message), + logWarn: (message: string) => deps.logWarn(message), + }); +} + +export function createBuildStartBackgroundWarmupsMainDepsHandler(deps: StartBackgroundWarmupsMainDeps) { + return (): StartBackgroundWarmupsMainDeps => ({ + getStarted: () => deps.getStarted(), + setStarted: (started: boolean) => deps.setStarted(started), + isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(), + launchTask: (label: string, task: () => Promise) => deps.launchTask(label, task), + createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(), + ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(), + prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(), + shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(), + startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(), + }); +} diff --git a/src/main/runtime/subtitle-position-main-deps.test.ts b/src/main/runtime/subtitle-position-main-deps.test.ts new file mode 100644 index 0000000..e2e2ee6 --- /dev/null +++ b/src/main/runtime/subtitle-position-main-deps.test.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildLoadSubtitlePositionMainDepsHandler, + createBuildSaveSubtitlePositionMainDepsHandler, +} from './subtitle-position-main-deps'; + +test('load subtitle position main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildLoadSubtitlePositionMainDepsHandler({ + loadSubtitlePositionCore: () => ({ x: 1, y: 2 } as never), + setSubtitlePosition: () => calls.push('set'), + })(); + + assert.deepEqual(deps.loadSubtitlePositionCore(), { x: 1, y: 2 }); + deps.setSubtitlePosition({ x: 3, y: 4 } as never); + assert.deepEqual(calls, ['set']); +}); + +test('save subtitle position main deps builder maps callbacks', () => { + const calls: string[] = []; + const deps = createBuildSaveSubtitlePositionMainDepsHandler({ + saveSubtitlePositionCore: () => calls.push('persist'), + setSubtitlePosition: () => calls.push('set'), + })(); + + deps.setSubtitlePosition({ x: 1, y: 2 } as never); + deps.saveSubtitlePositionCore({ x: 1, y: 2 } as never); + assert.deepEqual(calls, ['set', 'persist']); +}); diff --git a/src/main/runtime/subtitle-position-main-deps.ts b/src/main/runtime/subtitle-position-main-deps.ts new file mode 100644 index 0000000..9e01ba8 --- /dev/null +++ b/src/main/runtime/subtitle-position-main-deps.ts @@ -0,0 +1,21 @@ +import type { + createLoadSubtitlePositionHandler, + createSaveSubtitlePositionHandler, +} from './subtitle-position'; + +type LoadSubtitlePositionMainDeps = Parameters[0]; +type SaveSubtitlePositionMainDeps = Parameters[0]; + +export function createBuildLoadSubtitlePositionMainDepsHandler(deps: LoadSubtitlePositionMainDeps) { + return (): LoadSubtitlePositionMainDeps => ({ + loadSubtitlePositionCore: () => deps.loadSubtitlePositionCore(), + setSubtitlePosition: (position) => deps.setSubtitlePosition(position), + }); +} + +export function createBuildSaveSubtitlePositionMainDepsHandler(deps: SaveSubtitlePositionMainDeps) { + return (): SaveSubtitlePositionMainDeps => ({ + saveSubtitlePositionCore: (position) => deps.saveSubtitlePositionCore(position), + setSubtitlePosition: (position) => deps.setSubtitlePosition(position), + }); +} diff --git a/src/main/runtime/yomitan-extension-loader-main-deps.test.ts b/src/main/runtime/yomitan-extension-loader-main-deps.test.ts new file mode 100644 index 0000000..da97222 --- /dev/null +++ b/src/main/runtime/yomitan-extension-loader-main-deps.test.ts @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBuildEnsureYomitanExtensionLoadedMainDepsHandler, + createBuildLoadYomitanExtensionMainDepsHandler, +} from './yomitan-extension-loader-main-deps'; + +test('load yomitan extension main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildLoadYomitanExtensionMainDepsHandler({ + loadYomitanExtensionCore: async () => { + calls.push('load-core'); + return null; + }, + userDataPath: '/tmp/subminer', + getYomitanParserWindow: () => null, + setYomitanParserWindow: () => calls.push('set-window'), + setYomitanParserReadyPromise: () => calls.push('set-ready'), + setYomitanParserInitPromise: () => calls.push('set-init'), + setYomitanExtension: () => calls.push('set-ext'), + })(); + + assert.equal(deps.userDataPath, '/tmp/subminer'); + await deps.loadYomitanExtensionCore({} as never); + deps.setYomitanParserWindow(null); + deps.setYomitanParserReadyPromise(null); + deps.setYomitanParserInitPromise(null); + deps.setYomitanExtension(null); + assert.deepEqual(calls, ['load-core', 'set-window', 'set-ready', 'set-init', 'set-ext']); +}); + +test('ensure yomitan extension loaded main deps builder maps callbacks', async () => { + const calls: string[] = []; + const deps = createBuildEnsureYomitanExtensionLoadedMainDepsHandler({ + getYomitanExtension: () => null, + getLoadInFlight: () => null, + setLoadInFlight: () => calls.push('set-inflight'), + loadYomitanExtension: async () => { + calls.push('load'); + return null; + }, + })(); + + assert.equal(deps.getYomitanExtension(), null); + assert.equal(deps.getLoadInFlight(), null); + deps.setLoadInFlight(null); + await deps.loadYomitanExtension(); + assert.deepEqual(calls, ['set-inflight', 'load']); +}); diff --git a/src/main/runtime/yomitan-extension-loader-main-deps.ts b/src/main/runtime/yomitan-extension-loader-main-deps.ts new file mode 100644 index 0000000..cb28a29 --- /dev/null +++ b/src/main/runtime/yomitan-extension-loader-main-deps.ts @@ -0,0 +1,34 @@ +import type { + createEnsureYomitanExtensionLoadedHandler, + createLoadYomitanExtensionHandler, +} from './yomitan-extension-loader'; + +type LoadYomitanExtensionMainDeps = Parameters[0]; +type EnsureYomitanExtensionLoadedMainDeps = Parameters< + typeof createEnsureYomitanExtensionLoadedHandler +>[0]; + +export function createBuildLoadYomitanExtensionMainDepsHandler( + deps: LoadYomitanExtensionMainDeps, +) { + return (): LoadYomitanExtensionMainDeps => ({ + loadYomitanExtensionCore: (options) => deps.loadYomitanExtensionCore(options), + userDataPath: deps.userDataPath, + getYomitanParserWindow: () => deps.getYomitanParserWindow(), + setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window), + setYomitanParserReadyPromise: (promise) => deps.setYomitanParserReadyPromise(promise), + setYomitanParserInitPromise: (promise) => deps.setYomitanParserInitPromise(promise), + setYomitanExtension: (extension) => deps.setYomitanExtension(extension), + }); +} + +export function createBuildEnsureYomitanExtensionLoadedMainDepsHandler( + deps: EnsureYomitanExtensionLoadedMainDeps, +) { + return (): EnsureYomitanExtensionLoadedMainDeps => ({ + getYomitanExtension: () => deps.getYomitanExtension(), + getLoadInFlight: () => deps.getLoadInFlight(), + setLoadInFlight: (promise) => deps.setLoadInFlight(promise), + loadYomitanExtension: () => deps.loadYomitanExtension(), + }); +}