refactor: extract additional main dependency builders

This commit is contained in:
2026-02-20 01:02:40 -08:00
parent 5476d44005
commit 18648cb6fc
45 changed files with 2516 additions and 159 deletions

View File

@@ -6,7 +6,7 @@ Read first. Keep concise.
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- | | ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | | `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-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-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` | | `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` | | `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |

View File

@@ -9,6 +9,22 @@
## Current Work (newest first) ## 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: 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] 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). - [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: 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] 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-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.

File diff suppressed because it is too large Load Diff

View File

@@ -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']);
});

View File

@@ -0,0 +1,31 @@
import type {
createEnsureAnilistMediaGuessHandler,
createMaybeProbeAnilistDurationHandler,
} from './anilist-media-guess';
type MaybeProbeAnilistDurationMainDeps = Parameters<typeof createMaybeProbeAnilistDurationHandler>[0];
type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[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),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,72 @@
import type {
createGetAnilistMediaGuessRuntimeStateHandler,
createGetCurrentAnilistMediaKeyHandler,
createResetAnilistMediaGuessStateHandler,
createResetAnilistMediaTrackingHandler,
createSetAnilistMediaGuessRuntimeStateHandler,
} from './anilist-media-state';
type GetCurrentAnilistMediaKeyMainDeps = Parameters<typeof createGetCurrentAnilistMediaKeyHandler>[0];
type ResetAnilistMediaTrackingMainDeps = Parameters<typeof createResetAnilistMediaTrackingHandler>[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),
});
}

View File

@@ -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',
]);
});

View File

@@ -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,
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,27 @@
import type { createOpenAnilistSetupWindowHandler } from './anilist-setup-window';
type OpenAnilistSetupWindowMainDeps = Parameters<typeof createOpenAnilistSetupWindowHandler>[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),
});
}

View File

@@ -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']);
});

View File

@@ -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(),
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,88 @@
import type { createRefreshKnownWordCacheHandler } from './anki-actions';
type RefreshKnownWordCacheMainDeps = Parameters<typeof createRefreshKnownWordCacheHandler>[0];
export function createBuildUpdateLastCardFromClipboardMainDepsHandler<TAnki>(deps: {
getAnkiIntegration: () => TAnki;
readClipboardText: () => string;
showMpvOsd: (text: string) => void;
updateLastCardFromClipboardCore: (options: {
ankiIntegration: TAnki;
readClipboardText: () => string;
showMpvOsd: (text: string) => void;
}) => Promise<void>;
}) {
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<TAnki>(deps: {
getAnkiIntegration: () => TAnki;
showMpvOsd: (text: string) => void;
triggerFieldGroupingCore: (options: {
ankiIntegration: TAnki;
showMpvOsd: (text: string) => void;
}) => Promise<void>;
}) {
return () => ({
getAnkiIntegration: () => deps.getAnkiIntegration(),
showMpvOsd: (text: string) => deps.showMpvOsd(text),
triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
deps.triggerFieldGroupingCore(options),
});
}
export function createBuildMarkLastCardAsAudioCardMainDepsHandler<TAnki>(deps: {
getAnkiIntegration: () => TAnki;
showMpvOsd: (text: string) => void;
markLastCardAsAudioCardCore: (options: {
ankiIntegration: TAnki;
showMpvOsd: (text: string) => void;
}) => Promise<void>;
}) {
return () => ({
getAnkiIntegration: () => deps.getAnkiIntegration(),
showMpvOsd: (text: string) => deps.showMpvOsd(text),
markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
deps.markLastCardAsAudioCardCore(options),
});
}
export function createBuildMineSentenceCardMainDepsHandler<TAnki, TMpv>(deps: {
getAnkiIntegration: () => TAnki;
getMpvClient: () => TMpv;
showMpvOsd: (text: string) => void;
mineSentenceCardCore: (options: {
ankiIntegration: TAnki;
mpvClient: TMpv;
showMpvOsd: (text: string) => void;
}) => Promise<boolean>;
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),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,25 @@
import type {
createGetFieldGroupingResolverHandler,
createSetFieldGroupingResolverHandler,
} from './field-grouping-resolver';
type GetFieldGroupingResolverMainDeps = Parameters<typeof createGetFieldGroupingResolverHandler>[0];
type SetFieldGroupingResolverMainDeps = Parameters<typeof createSetFieldGroupingResolverHandler>[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(),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,21 @@
import type { createHandleMpvCommandFromIpcHandler } from './ipc-bridge-actions';
type HandleMpvCommandFromIpcMainDeps = Parameters<typeof createHandleMpvCommandFromIpcHandler>[0];
export function createBuildHandleMpvCommandFromIpcMainDepsHandler(
deps: HandleMpvCommandFromIpcMainDeps,
) {
return (): HandleMpvCommandFromIpcMainDeps => ({
handleMpvCommandFromIpcRuntime: (command, options) =>
deps.handleMpvCommandFromIpcRuntime(command, options),
buildMpvCommandDeps: () => deps.buildMpvCommandDeps(),
});
}
export function createBuildRunSubsyncManualFromIpcMainDepsHandler<TRequest, TResult>(deps: {
runManualFromIpc: (request: TRequest) => Promise<TResult>;
}) {
return () => ({
runManualFromIpc: (request: TRequest) => deps.runManualFromIpc(request),
});
}

View File

@@ -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',
]);
});

View File

@@ -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(),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,36 @@
import type {
createStartJellyfinRemoteSessionHandler,
createStopJellyfinRemoteSessionHandler,
} from './jellyfin-remote-session-lifecycle';
type StartJellyfinRemoteSessionMainDeps = Parameters<typeof createStartJellyfinRemoteSessionHandler>[0];
type StopJellyfinRemoteSessionMainDeps = Parameters<typeof createStopJellyfinRemoteSessionHandler>[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(),
});
}

View File

@@ -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: () => '<html></html>',
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'), '<html></html>');
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']);
});

View File

@@ -0,0 +1,26 @@
import type { createOpenJellyfinSetupWindowHandler } from './jellyfin-setup-window';
type OpenJellyfinSetupWindowMainDeps = Parameters<typeof createOpenJellyfinSetupWindowHandler>[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),
});
}

View File

@@ -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']);
});

View File

@@ -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),
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,92 @@
export function createBuildHandleMultiCopyDigitMainDepsHandler<TSubtitleTimingTracker>(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<TSubtitleTimingTracker>(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),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,25 @@
import type {
createCancelNumericShortcutSessionHandler,
createStartNumericShortcutSessionHandler,
} from './numeric-shortcut-session-handlers';
type CancelNumericShortcutSessionMainDeps = Parameters<typeof createCancelNumericShortcutSessionHandler>[0];
type StartNumericShortcutSessionMainDeps = Parameters<typeof createStartNumericShortcutSessionHandler>[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,
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,43 @@
import type {
createAppendClipboardVideoToQueueHandler,
createHandleOverlayModalClosedHandler,
createSetOverlayVisibleHandler,
createToggleOverlayHandler,
} from './overlay-main-actions';
type SetOverlayVisibleMainDeps = Parameters<typeof createSetOverlayVisibleHandler>[0];
type ToggleOverlayMainDeps = Parameters<typeof createToggleOverlayHandler>[0];
type HandleOverlayModalClosedMainDeps = Parameters<typeof createHandleOverlayModalClosedHandler>[0];
type AppendClipboardVideoToQueueMainDeps = Parameters<typeof createAppendClipboardVideoToQueueHandler>[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),
});
}

View File

@@ -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);
});

View File

@@ -0,0 +1,41 @@
import type {
createRefreshOverlayShortcutsHandler,
createRegisterOverlayShortcutsHandler,
createSyncOverlayShortcutsHandler,
createUnregisterOverlayShortcutsHandler,
} from './overlay-shortcuts-lifecycle';
type RegisterOverlayShortcutsMainDeps = Parameters<typeof createRegisterOverlayShortcutsHandler>[0];
type UnregisterOverlayShortcutsMainDeps = Parameters<typeof createUnregisterOverlayShortcutsHandler>[0];
type SyncOverlayShortcutsMainDeps = Parameters<typeof createSyncOverlayShortcutsHandler>[0];
type RefreshOverlayShortcutsMainDeps = Parameters<typeof createRefreshOverlayShortcutsHandler>[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,
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,53 @@
import type {
createSetInvisibleOverlayVisibleHandler,
createSetVisibleOverlayVisibleHandler,
createToggleInvisibleOverlayHandler,
createToggleVisibleOverlayHandler,
} from './overlay-visibility-actions';
type SetVisibleOverlayVisibleMainDeps = Parameters<typeof createSetVisibleOverlayVisibleHandler>[0];
type SetInvisibleOverlayVisibleMainDeps = Parameters<typeof createSetInvisibleOverlayVisibleHandler>[0];
type ToggleVisibleOverlayMainDeps = Parameters<typeof createToggleVisibleOverlayHandler>[0];
type ToggleInvisibleOverlayMainDeps = Parameters<typeof createToggleInvisibleOverlayHandler>[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),
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,48 @@
import type {
createEnforceOverlayLayerOrderHandler,
createEnsureOverlayWindowLevelHandler,
createUpdateInvisibleOverlayBoundsHandler,
createUpdateVisibleOverlayBoundsHandler,
} from './overlay-window-layout';
type UpdateVisibleOverlayBoundsMainDeps = Parameters<typeof createUpdateVisibleOverlayBoundsHandler>[0];
type UpdateInvisibleOverlayBoundsMainDeps = Parameters<typeof createUpdateInvisibleOverlayBoundsHandler>[0];
type EnsureOverlayWindowLevelMainDeps = Parameters<typeof createEnsureOverlayWindowLevelHandler>[0];
type EnforceOverlayLayerOrderMainDeps = Parameters<typeof createEnforceOverlayLayerOrderHandler>[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),
});
}

View File

@@ -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',
]);
});

View File

@@ -0,0 +1,31 @@
import type {
createLaunchBackgroundWarmupTaskHandler,
createStartBackgroundWarmupsHandler,
} from './startup-warmups';
type LaunchBackgroundWarmupTaskMainDeps = Parameters<typeof createLaunchBackgroundWarmupTaskHandler>[0];
type StartBackgroundWarmupsMainDeps = Parameters<typeof createStartBackgroundWarmupsHandler>[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<void>) => deps.launchTask(label, task),
createMecabTokenizerAndCheck: () => deps.createMecabTokenizerAndCheck(),
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
prewarmSubtitleDictionaries: () => deps.prewarmSubtitleDictionaries(),
shouldAutoConnectJellyfinRemote: () => deps.shouldAutoConnectJellyfinRemote(),
startJellyfinRemoteSession: () => deps.startJellyfinRemoteSession(),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,21 @@
import type {
createLoadSubtitlePositionHandler,
createSaveSubtitlePositionHandler,
} from './subtitle-position';
type LoadSubtitlePositionMainDeps = Parameters<typeof createLoadSubtitlePositionHandler>[0];
type SaveSubtitlePositionMainDeps = Parameters<typeof createSaveSubtitlePositionHandler>[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),
});
}

View File

@@ -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']);
});

View File

@@ -0,0 +1,34 @@
import type {
createEnsureYomitanExtensionLoadedHandler,
createLoadYomitanExtensionHandler,
} from './yomitan-extension-loader';
type LoadYomitanExtensionMainDeps = Parameters<typeof createLoadYomitanExtensionHandler>[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(),
});
}