mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract main runtime dependency builders
This commit is contained in:
@@ -6,7 +6,7 @@ Read first. Keep concise.
|
|||||||
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
| ------------ | -------------- | ---------------------------------------------------- | --------- | ------------------------------------- | ---------------------- |
|
||||||
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
|
||||||
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
|
||||||
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T05:50:43Z` |
|
| `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-20T06:56:20Z` |
|
||||||
| `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` |
|
||||||
|
|||||||
@@ -193,6 +193,28 @@
|
|||||||
- `src/main/runtime/initial-args-main-deps.test.ts`
|
- `src/main/runtime/initial-args-main-deps.test.ts`
|
||||||
- `src/main/runtime/mpv-main-event-main-deps.ts`
|
- `src/main/runtime/mpv-main-event-main-deps.ts`
|
||||||
- `src/main/runtime/mpv-main-event-main-deps.test.ts`
|
- `src/main/runtime/mpv-main-event-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/subtitle-tokenization-main-deps.ts`
|
||||||
|
- `src/main/runtime/subtitle-tokenization-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/secondary-sub-mode-main-deps.ts`
|
||||||
|
- `src/main/runtime/secondary-sub-mode-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/overlay-runtime-main-actions.ts`
|
||||||
|
- `src/main/runtime/overlay-runtime-main-actions.test.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info.ts`
|
||||||
|
- `src/main/runtime/jellyfin-client-info.test.ts`
|
||||||
|
- `src/main/runtime/startup-config-main-deps.ts`
|
||||||
|
- `src/main/runtime/startup-config-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/app-ready-main-deps.ts`
|
||||||
|
- `src/main/runtime/app-ready-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/startup-lifecycle-main-deps.ts`
|
||||||
|
- `src/main/runtime/startup-lifecycle-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/startup-bootstrap-main-deps.ts`
|
||||||
|
- `src/main/runtime/startup-bootstrap-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/cli-command-prechecks-main-deps.ts`
|
||||||
|
- `src/main/runtime/cli-command-prechecks-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/field-grouping-resolver.ts`
|
||||||
|
- `src/main/runtime/field-grouping-resolver.test.ts`
|
||||||
|
- `src/main/runtime/mpv-jellyfin-defaults.ts`
|
||||||
|
- `src/main/runtime/mpv-jellyfin-defaults.test.ts`
|
||||||
|
|
||||||
## Open Questions / Blockers
|
## Open Questions / Blockers
|
||||||
|
|
||||||
@@ -244,3 +266,27 @@
|
|||||||
- [2026-02-20T05:15:40Z] progress: extracted MPV main-event deps assembly into `src/main/runtime/mpv-main-event-main-deps.ts` (`createBuildBindMpvMainEventHandlersMainDepsHandler`) and rewired `bindMpvClientEventHandlers` setup in `src/main.ts`.
|
- [2026-02-20T05:15:40Z] progress: extracted MPV main-event deps assembly into `src/main/runtime/mpv-main-event-main-deps.ts` (`createBuildBindMpvMainEventHandlersMainDepsHandler`) and rewired `bindMpvClientEventHandlers` setup in `src/main.ts`.
|
||||||
- [2026-02-20T05:15:40Z] progress: `src/main.ts` currently 2653 LOC after this slice.
|
- [2026-02-20T05:15:40Z] progress: `src/main.ts` currently 2653 LOC after this slice.
|
||||||
- [2026-02-20T05:15:40Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/app-lifecycle-main-activate.test.js dist/main/runtime/initial-args-main-deps.test.js dist/main/runtime/app-lifecycle-actions.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/app-runtime-main-deps.test.js dist/main/runtime/mpv-main-event-main-deps.test.js dist/main/runtime/mpv-main-event-bindings.test.js dist/main/runtime/mpv-main-event-actions.test.js dist/main/runtime/mpv-client-event-bindings.test.js` pass (27/27).
|
- [2026-02-20T05:15:40Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/app-lifecycle-main-activate.test.js dist/main/runtime/initial-args-main-deps.test.js dist/main/runtime/app-lifecycle-actions.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/app-runtime-main-deps.test.js dist/main/runtime/mpv-main-event-main-deps.test.js dist/main/runtime/mpv-main-event-bindings.test.js dist/main/runtime/mpv-main-event-actions.test.js dist/main/runtime/mpv-client-event-bindings.test.js` pass (27/27).
|
||||||
|
- [2026-02-20T05:31:05Z] progress: extracted subtitle tokenization/mecab-warmup deps assembly into `src/main/runtime/subtitle-tokenization-main-deps.ts` (`createBuildTokenizerDepsMainHandler`, `createCreateMecabTokenizerAndCheckMainHandler`, `createPrewarmSubtitleDictionariesMainHandler`) and rewired `tokenizeSubtitle`, `createMecabTokenizerAndCheck`, and `prewarmSubtitleDictionaries` in `src/main.ts`.
|
||||||
|
- [2026-02-20T05:31:05Z] progress: extracted `cycleSecondarySubMode` deps assembly into `src/main/runtime/secondary-sub-mode-main-deps.ts` (`createBuildCycleSecondarySubModeMainDepsHandler`) and rewired `cycleSecondarySubMode` in `src/main.ts`.
|
||||||
|
- [2026-02-20T05:31:05Z] progress: while implementing subtitle-tokenization deps, fixed strict typing to align parser promise shapes (`Promise<void>` ready + `Promise<boolean>` init) and Mecab availability return type.
|
||||||
|
- [2026-02-20T05:31:05Z] progress: `src/main.ts` currently 2664 LOC after this slice.
|
||||||
|
- [2026-02-20T05:31:05Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/subtitle-tokenization-main-deps.test.js dist/main/runtime/secondary-sub-mode-main-deps.test.js dist/main/runtime/startup-warmups.test.js` pass (7/7).
|
||||||
|
- [2026-02-20T05:49:13Z] progress: extracted overlay runtime state/action wrappers into `src/main/runtime/overlay-runtime-main-actions.ts` (`createGetRuntimeOptionsStateHandler`, `createRestorePreviousSecondarySubVisibilityHandler`, `createBroadcastRuntimeOptionsChangedHandler`, `createSendToActiveOverlayWindowHandler`, `createSetOverlayDebugVisualizationEnabledHandler`, `createOpenRuntimeOptionsPaletteHandler`), and rewired corresponding `main.ts` functions to thin delegations.
|
||||||
|
- [2026-02-20T05:49:13Z] progress: while adding tests, corrected strict typing to import `OverlayHostedModal` from `src/main/overlay-runtime.ts` and aligned `RuntimeOptionState` fixture shape.
|
||||||
|
- [2026-02-20T05:49:13Z] progress: `src/main.ts` currently 2696 LOC after this slice.
|
||||||
|
- [2026-02-20T05:49:13Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-runtime-main-actions.test.js dist/main/runtime/subtitle-tokenization-main-deps.test.js dist/main/runtime/secondary-sub-mode-main-deps.test.js` pass (12/12).
|
||||||
|
- [2026-02-20T05:50:43Z] progress: extracted Jellyfin config/client info wrappers into `src/main/runtime/jellyfin-client-info.ts` (`createGetResolvedJellyfinConfigHandler`, `createGetJellyfinClientInfoHandler`) and rewired `getResolvedJellyfinConfig` + `getJellyfinClientInfo` in `src/main.ts`.
|
||||||
|
- [2026-02-20T05:50:43Z] progress: fixed strict typing regressions by loosening helper input types to preserve call-site inference while keeping client-info output normalized to strings.
|
||||||
|
- [2026-02-20T05:50:43Z] progress: `src/main.ts` currently 2702 LOC after this slice.
|
||||||
|
- [2026-02-20T05:50:43Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-client-info.test.js dist/main/runtime/overlay-runtime-main-actions.test.js dist/main/runtime/subtitle-tokenization-main-deps.test.js` pass (14/14).
|
||||||
|
- [2026-02-20T06:52:54Z] progress: extracted startup config handler deps assembly into `src/main/runtime/startup-config-main-deps.ts` and rewired `reloadConfigHandler` + `criticalConfigErrorHandler` setup in `src/main.ts`.
|
||||||
|
- [2026-02-20T06:52:54Z] progress: extracted app-ready runner deps assembly into `src/main/runtime/app-ready-main-deps.ts` and lifted nested `createAppReadyRuntimeRunner(...)` input block to `appReadyRuntimeRunner` constant in `src/main.ts`.
|
||||||
|
- [2026-02-20T06:52:54Z] progress: extracted app-lifecycle runner deps assembly into `src/main/runtime/startup-lifecycle-main-deps.ts` and lifted lifecycle runner wiring to `appLifecycleRuntimeRunner` constant in `src/main.ts`.
|
||||||
|
- [2026-02-20T06:52:54Z] progress: extracted startup bootstrap deps assembly into `src/main/runtime/startup-bootstrap-main-deps.ts` and rewired `buildStartupBootstrapRuntimeFactoryDepsHandler` to compose through the new builder.
|
||||||
|
- [2026-02-20T06:52:54Z] progress: `src/main.ts` currently 2723 LOC after this slice.
|
||||||
|
- [2026-02-20T06:52:54Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/startup-bootstrap-main-deps.test.js dist/main/runtime/startup-bootstrap-deps-builder.test.js dist/main/runtime/startup-lifecycle-main-deps.test.js dist/main/runtime/app-ready-main-deps.test.js dist/main/runtime/startup-config-main-deps.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/app-ready.test.js` pass (21/21).
|
||||||
|
- [2026-02-20T06:56:20Z] progress: extracted CLI precheck deps assembly into `src/main/runtime/cli-command-prechecks-main-deps.ts` and rewired `handleCliCommand` to use a prebuilt `handleTexthookerOnlyModeTransitionHandler`.
|
||||||
|
- [2026-02-20T06:56:20Z] progress: extracted field-grouping resolver state wrappers into `src/main/runtime/field-grouping-resolver.ts` and rewired `getFieldGroupingResolver` + `setFieldGroupingResolver` in `src/main.ts`.
|
||||||
|
- [2026-02-20T06:56:20Z] progress: extracted `applyJellyfinMpvDefaults` and `getDefaultSocketPath` wrappers into `src/main/runtime/mpv-jellyfin-defaults.ts`; rewired both `main.ts` helper functions to thin handler delegates.
|
||||||
|
- [2026-02-20T06:56:20Z] progress: `src/main.ts` currently 2742 LOC after this slice.
|
||||||
|
- [2026-02-20T06:56:20Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/cli-command-prechecks-main-deps.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/field-grouping-resolver.test.js dist/main/runtime/mpv-jellyfin-defaults.test.js dist/main/runtime/startup-bootstrap-main-deps.test.js` pass (11/11).
|
||||||
|
|||||||
611
src/main.ts
611
src/main.ts
@@ -148,6 +148,14 @@ import { createRunJellyfinCommandHandler } from './main/runtime/jellyfin-command
|
|||||||
import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list';
|
import { createHandleJellyfinListCommands } from './main/runtime/jellyfin-cli-list';
|
||||||
import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play';
|
import { createHandleJellyfinPlayCommand } from './main/runtime/jellyfin-cli-play';
|
||||||
import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce';
|
import { createHandleJellyfinRemoteAnnounceCommand } from './main/runtime/jellyfin-cli-remote-announce';
|
||||||
|
import {
|
||||||
|
createGetJellyfinClientInfoHandler,
|
||||||
|
createGetResolvedJellyfinConfigHandler,
|
||||||
|
} from './main/runtime/jellyfin-client-info';
|
||||||
|
import {
|
||||||
|
createApplyJellyfinMpvDefaultsHandler,
|
||||||
|
createGetDefaultSocketPathHandler,
|
||||||
|
} from './main/runtime/mpv-jellyfin-defaults';
|
||||||
import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch';
|
import { createPlayJellyfinItemInMpvHandler } from './main/runtime/jellyfin-playback-launch';
|
||||||
import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload';
|
import { createPreloadJellyfinExternalSubtitlesHandler } from './main/runtime/jellyfin-subtitle-preload';
|
||||||
import {
|
import {
|
||||||
@@ -157,12 +165,22 @@ import {
|
|||||||
import { createHandleInitialArgsHandler } from './main/runtime/initial-args-handler';
|
import { createHandleInitialArgsHandler } from './main/runtime/initial-args-handler';
|
||||||
import { createBuildHandleInitialArgsMainDepsHandler } from './main/runtime/initial-args-main-deps';
|
import { createBuildHandleInitialArgsMainDepsHandler } from './main/runtime/initial-args-main-deps';
|
||||||
import { createHandleTexthookerOnlyModeTransitionHandler } from './main/runtime/cli-command-prechecks';
|
import { createHandleTexthookerOnlyModeTransitionHandler } from './main/runtime/cli-command-prechecks';
|
||||||
|
import { createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler } from './main/runtime/cli-command-prechecks-main-deps';
|
||||||
|
import {
|
||||||
|
createGetFieldGroupingResolverHandler,
|
||||||
|
createSetFieldGroupingResolverHandler,
|
||||||
|
} from './main/runtime/field-grouping-resolver';
|
||||||
import { createCliCommandContext } from './main/runtime/cli-command-context';
|
import { createCliCommandContext } from './main/runtime/cli-command-context';
|
||||||
import { createBindMpvMainEventHandlersHandler } from './main/runtime/mpv-main-event-bindings';
|
import { createBindMpvMainEventHandlersHandler } from './main/runtime/mpv-main-event-bindings';
|
||||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runtime/mpv-main-event-main-deps';
|
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './main/runtime/mpv-main-event-main-deps';
|
||||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/mpv-client-runtime-service-main-deps';
|
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from './main/runtime/mpv-client-runtime-service-main-deps';
|
||||||
import { createMpvClientRuntimeServiceFactory } from './main/runtime/mpv-client-runtime-service';
|
import { createMpvClientRuntimeServiceFactory } from './main/runtime/mpv-client-runtime-service';
|
||||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/mpv-subtitle-render-metrics';
|
import { createUpdateMpvSubtitleRenderMetricsHandler } from './main/runtime/mpv-subtitle-render-metrics';
|
||||||
|
import {
|
||||||
|
createBuildTokenizerDepsMainHandler,
|
||||||
|
createCreateMecabTokenizerAndCheckMainHandler,
|
||||||
|
createPrewarmSubtitleDictionariesMainHandler,
|
||||||
|
} from './main/runtime/subtitle-tokenization-main-deps';
|
||||||
import {
|
import {
|
||||||
createLaunchBackgroundWarmupTaskHandler,
|
createLaunchBackgroundWarmupTaskHandler,
|
||||||
createStartBackgroundWarmupsHandler,
|
createStartBackgroundWarmupsHandler,
|
||||||
@@ -192,6 +210,7 @@ import {
|
|||||||
createBuildAppendToMpvLogMainDepsHandler,
|
createBuildAppendToMpvLogMainDepsHandler,
|
||||||
createBuildShowMpvOsdMainDepsHandler,
|
createBuildShowMpvOsdMainDepsHandler,
|
||||||
} from './main/runtime/mpv-osd-log-main-deps';
|
} from './main/runtime/mpv-osd-log-main-deps';
|
||||||
|
import { createBuildCycleSecondarySubModeMainDepsHandler } from './main/runtime/secondary-sub-mode-main-deps';
|
||||||
import {
|
import {
|
||||||
createCancelNumericShortcutSessionHandler,
|
createCancelNumericShortcutSessionHandler,
|
||||||
createStartNumericShortcutSessionHandler,
|
createStartNumericShortcutSessionHandler,
|
||||||
@@ -226,6 +245,14 @@ import {
|
|||||||
createSetOverlayVisibleHandler,
|
createSetOverlayVisibleHandler,
|
||||||
createToggleOverlayHandler,
|
createToggleOverlayHandler,
|
||||||
} from './main/runtime/overlay-main-actions';
|
} from './main/runtime/overlay-main-actions';
|
||||||
|
import {
|
||||||
|
createBroadcastRuntimeOptionsChangedHandler,
|
||||||
|
createGetRuntimeOptionsStateHandler,
|
||||||
|
createOpenRuntimeOptionsPaletteHandler,
|
||||||
|
createRestorePreviousSecondarySubVisibilityHandler,
|
||||||
|
createSendToActiveOverlayWindowHandler,
|
||||||
|
createSetOverlayDebugVisualizationEnabledHandler,
|
||||||
|
} from './main/runtime/overlay-runtime-main-actions';
|
||||||
import {
|
import {
|
||||||
createHandleMpvCommandFromIpcHandler,
|
createHandleMpvCommandFromIpcHandler,
|
||||||
createRunSubsyncManualFromIpcHandler,
|
createRunSubsyncManualFromIpcHandler,
|
||||||
@@ -279,6 +306,13 @@ import {
|
|||||||
createConfigHotReloadMessageHandler,
|
createConfigHotReloadMessageHandler,
|
||||||
resolveSubtitleStyleForRenderer,
|
resolveSubtitleStyleForRenderer,
|
||||||
} from './main/runtime/config-hot-reload-handlers';
|
} from './main/runtime/config-hot-reload-handlers';
|
||||||
|
import {
|
||||||
|
createBuildCriticalConfigErrorMainDepsHandler,
|
||||||
|
createBuildReloadConfigMainDepsHandler,
|
||||||
|
} from './main/runtime/startup-config-main-deps';
|
||||||
|
import { createBuildAppReadyRuntimeMainDepsHandler } from './main/runtime/app-ready-main-deps';
|
||||||
|
import { createBuildAppLifecycleRuntimeRunnerMainDepsHandler } from './main/runtime/startup-lifecycle-main-deps';
|
||||||
|
import { createBuildStartupBootstrapMainDepsHandler } from './main/runtime/startup-bootstrap-main-deps';
|
||||||
import {
|
import {
|
||||||
enforceUnsupportedWaylandMode,
|
enforceUnsupportedWaylandMode,
|
||||||
forceX11Backend,
|
forceX11Backend,
|
||||||
@@ -429,14 +463,13 @@ let jellyfinMpvAutoLaunchInFlight: Promise<boolean> | null = null;
|
|||||||
let backgroundWarmupsStarted = false;
|
let backgroundWarmupsStarted = false;
|
||||||
let yomitanLoadInFlight: Promise<Extension | null> | null = null;
|
let yomitanLoadInFlight: Promise<Extension | null> | null = null;
|
||||||
|
|
||||||
|
const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler({
|
||||||
|
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command),
|
||||||
|
jellyfinLangPref: JELLYFIN_LANG_PREF,
|
||||||
|
});
|
||||||
|
|
||||||
function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
function applyJellyfinMpvDefaults(client: MpvIpcClient): void {
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
applyJellyfinMpvDefaultsHandler(client);
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'secondary-sid', 'auto']);
|
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'secondary-sub-visibility', 'no']);
|
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'alang', JELLYFIN_LANG_PREF]);
|
|
||||||
sendMpvCommandRuntime(client, ['set_property', 'slang', JELLYFIN_LANG_PREF]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = resolveConfigDir({
|
const CONFIG_DIR = resolveConfigDir({
|
||||||
@@ -493,11 +526,12 @@ const appLogger = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultSocketPathHandler = createGetDefaultSocketPathHandler({
|
||||||
|
platform: process.platform,
|
||||||
|
});
|
||||||
|
|
||||||
function getDefaultSocketPath(): string {
|
function getDefaultSocketPath(): string {
|
||||||
if (process.platform === 'win32') {
|
return getDefaultSocketPathHandler();
|
||||||
return '\\\\.\\pipe\\subminer-socket';
|
|
||||||
}
|
|
||||||
return '/tmp/subminer-socket';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(USER_DATA_PATH)) {
|
if (!fs.existsSync(USER_DATA_PATH)) {
|
||||||
@@ -795,23 +829,29 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler({
|
||||||
|
getResolver: () => appState.fieldGroupingResolver,
|
||||||
|
});
|
||||||
|
|
||||||
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
function getFieldGroupingResolver(): ((choice: KikuFieldGroupingChoice) => void) | null {
|
||||||
return appState.fieldGroupingResolver;
|
return getFieldGroupingResolverHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setFieldGroupingResolverHandler = createSetFieldGroupingResolverHandler({
|
||||||
|
setResolver: (resolver) => {
|
||||||
|
appState.fieldGroupingResolver = resolver;
|
||||||
|
},
|
||||||
|
nextSequence: () => {
|
||||||
|
appState.fieldGroupingResolverSequence += 1;
|
||||||
|
return appState.fieldGroupingResolverSequence;
|
||||||
|
},
|
||||||
|
getSequence: () => appState.fieldGroupingResolverSequence,
|
||||||
|
});
|
||||||
|
|
||||||
function setFieldGroupingResolver(
|
function setFieldGroupingResolver(
|
||||||
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
resolver: ((choice: KikuFieldGroupingChoice) => void) | null,
|
||||||
): void {
|
): void {
|
||||||
if (!resolver) {
|
setFieldGroupingResolverHandler(resolver);
|
||||||
appState.fieldGroupingResolver = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sequence = ++appState.fieldGroupingResolverSequence;
|
|
||||||
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
|
|
||||||
if (sequence !== appState.fieldGroupingResolverSequence) return;
|
|
||||||
resolver(choice);
|
|
||||||
};
|
|
||||||
appState.fieldGroupingResolver = wrappedResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>({
|
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>({
|
||||||
@@ -880,71 +920,97 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler({
|
||||||
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
|
});
|
||||||
|
|
||||||
function getRuntimeOptionsState(): RuntimeOptionState[] {
|
function getRuntimeOptionsState(): RuntimeOptionState[] {
|
||||||
if (!appState.runtimeOptionsManager) return [];
|
return getRuntimeOptionsStateHandler();
|
||||||
return appState.runtimeOptionsManager.listOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOverlayWindows(): BrowserWindow[] {
|
function getOverlayWindows(): BrowserWindow[] {
|
||||||
return overlayManager.getOverlayWindows();
|
return overlayManager.getOverlayWindows();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const restorePreviousSecondarySubVisibilityHandler = createRestorePreviousSecondarySubVisibilityHandler(
|
||||||
|
{
|
||||||
|
getMpvClient: () => appState.mpvClient,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function restorePreviousSecondarySubVisibility(): void {
|
function restorePreviousSecondarySubVisibility(): void {
|
||||||
if (!appState.mpvClient || !appState.mpvClient.connected) return;
|
restorePreviousSecondarySubVisibilityHandler();
|
||||||
appState.mpvClient.restorePreviousSecondarySubVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||||
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const broadcastRuntimeOptionsChangedHandler = createBroadcastRuntimeOptionsChangedHandler({
|
||||||
|
broadcastRuntimeOptionsChangedRuntime,
|
||||||
|
getRuntimeOptionsState: () => getRuntimeOptionsState(),
|
||||||
|
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||||
|
});
|
||||||
|
|
||||||
function broadcastRuntimeOptionsChanged(): void {
|
function broadcastRuntimeOptionsChanged(): void {
|
||||||
broadcastRuntimeOptionsChangedRuntime(
|
broadcastRuntimeOptionsChangedHandler();
|
||||||
() => getRuntimeOptionsState(),
|
|
||||||
(channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendToActiveOverlayWindowHandler = createSendToActiveOverlayWindowHandler({
|
||||||
|
sendToActiveOverlayWindowRuntime: (channel, payload, runtimeOptions) =>
|
||||||
|
overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
|
});
|
||||||
|
|
||||||
function sendToActiveOverlayWindow(
|
function sendToActiveOverlayWindow(
|
||||||
channel: string,
|
channel: string,
|
||||||
payload?: unknown,
|
payload?: unknown,
|
||||||
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
): boolean {
|
): boolean {
|
||||||
return overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions);
|
return sendToActiveOverlayWindowHandler(channel, payload, runtimeOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
const setOverlayDebugVisualizationEnabledHandler = createSetOverlayDebugVisualizationEnabledHandler(
|
||||||
setOverlayDebugVisualizationEnabledRuntime(
|
{
|
||||||
appState.overlayDebugVisualizationEnabled,
|
setOverlayDebugVisualizationEnabledRuntime,
|
||||||
enabled,
|
getCurrentEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||||
(next) => {
|
setCurrentEnabled: (next) => {
|
||||||
appState.overlayDebugVisualizationEnabled = next;
|
appState.overlayDebugVisualizationEnabled = next;
|
||||||
},
|
},
|
||||||
(channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function setOverlayDebugVisualizationEnabled(enabled: boolean): void {
|
||||||
|
setOverlayDebugVisualizationEnabledHandler(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openRuntimeOptionsPaletteHandler = createOpenRuntimeOptionsPaletteHandler({
|
||||||
|
openRuntimeOptionsPaletteRuntime: () => overlayModalRuntime.openRuntimeOptionsPalette(),
|
||||||
|
});
|
||||||
|
|
||||||
function openRuntimeOptionsPalette(): void {
|
function openRuntimeOptionsPalette(): void {
|
||||||
overlayModalRuntime.openRuntimeOptionsPalette();
|
openRuntimeOptionsPaletteHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResolvedConfig() {
|
function getResolvedConfig() {
|
||||||
return configService.getConfig();
|
return configService.getConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getResolvedJellyfinConfigHandler = createGetResolvedJellyfinConfigHandler({
|
||||||
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
});
|
||||||
|
|
||||||
function getResolvedJellyfinConfig() {
|
function getResolvedJellyfinConfig() {
|
||||||
return getResolvedConfig().jellyfin;
|
return getResolvedJellyfinConfigHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getJellyfinClientInfoHandler = createGetJellyfinClientInfoHandler({
|
||||||
|
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||||
|
getDefaultJellyfinConfig: () => DEFAULT_CONFIG.jellyfin,
|
||||||
|
});
|
||||||
|
|
||||||
function getJellyfinClientInfo(config = getResolvedJellyfinConfig()) {
|
function getJellyfinClientInfo(config = getResolvedJellyfinConfig()) {
|
||||||
const clientName = config.clientName || DEFAULT_CONFIG.jellyfin.clientName;
|
return getJellyfinClientInfoHandler(config);
|
||||||
const clientVersion = config.clientVersion || DEFAULT_CONFIG.jellyfin.clientVersion;
|
|
||||||
const deviceId = config.deviceId || DEFAULT_CONFIG.jellyfin.deviceId;
|
|
||||||
return {
|
|
||||||
clientName,
|
|
||||||
clientVersion,
|
|
||||||
deviceId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForMpvConnected = createWaitForMpvConnectedHandler({
|
const waitForMpvConnected = createWaitForMpvConnectedHandler({
|
||||||
@@ -1553,162 +1619,176 @@ const restoreWindowsOnActivateHandler = createRestoreWindowsOnActivateHandler(
|
|||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildStartupBootstrapRuntimeFactoryDepsHandler =
|
const reloadConfigHandler = createReloadConfigHandler(
|
||||||
createBuildStartupBootstrapRuntimeFactoryDepsHandler({
|
createBuildReloadConfigMainDepsHandler({
|
||||||
argv: process.argv,
|
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
logInfo: (message) => appLogger.logInfo(message),
|
||||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
logWarning: (message) => appLogger.logWarning(message),
|
||||||
setLogLevel(level, source);
|
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||||
|
startConfigHotReload: () => configHotReloadRuntime.start(),
|
||||||
|
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details) => logger.error(details),
|
||||||
|
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
||||||
|
quit: () => app.quit(),
|
||||||
},
|
},
|
||||||
forceX11Backend: (args: CliArgs) => {
|
})(),
|
||||||
forceX11Backend(args);
|
);
|
||||||
|
|
||||||
|
const criticalConfigErrorHandler = createCriticalConfigErrorHandler(
|
||||||
|
createBuildCriticalConfigErrorMainDepsHandler({
|
||||||
|
getConfigPath: () => configService.getConfigPath(),
|
||||||
|
failHandlers: {
|
||||||
|
logError: (message) => logger.error(message),
|
||||||
|
showErrorBox: (title, message) => dialog.showErrorBox(title, message),
|
||||||
|
quit: () => app.quit(),
|
||||||
},
|
},
|
||||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
})(),
|
||||||
enforceUnsupportedWaylandMode(args);
|
);
|
||||||
|
|
||||||
|
const appReadyRuntimeRunner = createAppReadyRuntimeRunner(
|
||||||
|
createBuildAppReadyRuntimeMainDepsHandler({
|
||||||
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
|
resolveKeybindings: () => {
|
||||||
|
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
||||||
},
|
},
|
||||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
createMpvClient: () => {
|
||||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
appState.mpvClient = createMpvClientRuntimeService();
|
||||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
},
|
||||||
configDir: CONFIG_DIR,
|
reloadConfig: reloadConfigHandler,
|
||||||
defaultConfig: DEFAULT_CONFIG,
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
getConfigWarnings: () => configService.getWarnings(),
|
||||||
generateDefaultConfigFile: (
|
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
||||||
args: CliArgs,
|
setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
|
||||||
options: {
|
initRuntimeOptionsManager: () => {
|
||||||
configDir: string;
|
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
||||||
defaultConfig: unknown;
|
() => configService.getConfig().ankiConnect,
|
||||||
generateTemplate: (config: unknown) => string;
|
{
|
||||||
|
applyAnkiPatch: (patch) => {
|
||||||
|
if (appState.ankiIntegration) {
|
||||||
|
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOptionsChanged: () => {
|
||||||
|
broadcastRuntimeOptionsChanged();
|
||||||
|
refreshOverlayShortcuts();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
|
appState.secondarySubMode = mode;
|
||||||
|
},
|
||||||
|
defaultSecondarySubMode: 'hover',
|
||||||
|
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||||
|
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
||||||
|
startSubtitleWebsocket: (port: number) => {
|
||||||
|
subtitleWsService.start(port, () => appState.currentSubText);
|
||||||
|
},
|
||||||
|
log: (message) => appLogger.logInfo(message),
|
||||||
|
createMecabTokenizerAndCheck: async () => {
|
||||||
|
await createMecabTokenizerAndCheck();
|
||||||
|
},
|
||||||
|
createSubtitleTimingTracker: () => {
|
||||||
|
const tracker = new SubtitleTimingTracker();
|
||||||
|
appState.subtitleTimingTracker = tracker;
|
||||||
|
},
|
||||||
|
createImmersionTracker: createImmersionTrackerStartupHandler({
|
||||||
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
|
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
||||||
|
createTrackerService: (params) => new ImmersionTrackerService(params),
|
||||||
|
setTracker: (tracker) => {
|
||||||
|
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
||||||
},
|
},
|
||||||
) => generateDefaultConfigFile(args, options),
|
getMpvClient: () => appState.mpvClient,
|
||||||
onConfigGenerated: (exitCode: number) => {
|
seedTrackerFromCurrentMedia: () => {
|
||||||
process.exitCode = exitCode;
|
void immersionMediaRuntime.seedFromCurrentMedia();
|
||||||
app.quit();
|
},
|
||||||
},
|
logInfo: (message) => logger.info(message),
|
||||||
onGenerateConfigError: (error: Error) => {
|
logDebug: (message) => logger.debug(message),
|
||||||
logger.error(`Failed to generate config: ${error.message}`);
|
logWarn: (message, details) => logger.warn(message, details),
|
||||||
process.exitCode = 1;
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
startAppLifecycle: createAppLifecycleRuntimeRunner({
|
|
||||||
app,
|
|
||||||
platform: process.platform,
|
|
||||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
|
||||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
|
||||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
|
||||||
handleCliCommand(nextArgs, source),
|
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
|
||||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
|
||||||
onReady: createAppReadyRuntimeRunner({
|
|
||||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
|
||||||
resolveKeybindings: () => {
|
|
||||||
appState.keybindings = resolveKeybindings(getResolvedConfig(), DEFAULT_KEYBINDINGS);
|
|
||||||
},
|
|
||||||
createMpvClient: () => {
|
|
||||||
appState.mpvClient = createMpvClientRuntimeService();
|
|
||||||
},
|
|
||||||
reloadConfig: createReloadConfigHandler({
|
|
||||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
|
||||||
logInfo: (message) => appLogger.logInfo(message),
|
|
||||||
logWarning: (message) => appLogger.logWarning(message),
|
|
||||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
|
||||||
startConfigHotReload: () => configHotReloadRuntime.start(),
|
|
||||||
refreshAnilistClientSecretState: (options) => refreshAnilistClientSecretState(options),
|
|
||||||
failHandlers: {
|
|
||||||
logError: (details) => logger.error(details),
|
|
||||||
showErrorBox: (title, details) => dialog.showErrorBox(title, details),
|
|
||||||
quit: () => app.quit(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
|
||||||
getConfigWarnings: () => configService.getWarnings(),
|
|
||||||
logConfigWarning: (warning) => appLogger.logConfigWarning(warning),
|
|
||||||
setLogLevel: (level: string, source: LogLevelSource) => setLogLevel(level, source),
|
|
||||||
initRuntimeOptionsManager: () => {
|
|
||||||
appState.runtimeOptionsManager = new RuntimeOptionsManager(
|
|
||||||
() => configService.getConfig().ankiConnect,
|
|
||||||
{
|
|
||||||
applyAnkiPatch: (patch) => {
|
|
||||||
if (appState.ankiIntegration) {
|
|
||||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onOptionsChanged: () => {
|
|
||||||
broadcastRuntimeOptionsChanged();
|
|
||||||
refreshOverlayShortcuts();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
|
||||||
appState.secondarySubMode = mode;
|
|
||||||
},
|
|
||||||
defaultSecondarySubMode: 'hover',
|
|
||||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
|
||||||
hasMpvWebsocketPlugin: () => hasMpvWebsocketPlugin(),
|
|
||||||
startSubtitleWebsocket: (port: number) => {
|
|
||||||
subtitleWsService.start(port, () => appState.currentSubText);
|
|
||||||
},
|
|
||||||
log: (message) => appLogger.logInfo(message),
|
|
||||||
createMecabTokenizerAndCheck: async () => {
|
|
||||||
await createMecabTokenizerAndCheck();
|
|
||||||
},
|
|
||||||
createSubtitleTimingTracker: () => {
|
|
||||||
const tracker = new SubtitleTimingTracker();
|
|
||||||
appState.subtitleTimingTracker = tracker;
|
|
||||||
},
|
|
||||||
createImmersionTracker: createImmersionTrackerStartupHandler({
|
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
|
||||||
getConfiguredDbPath: () => immersionMediaRuntime.getConfiguredDbPath(),
|
|
||||||
createTrackerService: (params) => new ImmersionTrackerService(params),
|
|
||||||
setTracker: (tracker) => {
|
|
||||||
appState.immersionTracker = tracker as ImmersionTrackerService | null;
|
|
||||||
},
|
|
||||||
getMpvClient: () => appState.mpvClient,
|
|
||||||
seedTrackerFromCurrentMedia: () => {
|
|
||||||
void immersionMediaRuntime.seedFromCurrentMedia();
|
|
||||||
},
|
|
||||||
logInfo: (message) => logger.info(message),
|
|
||||||
logDebug: (message) => logger.debug(message),
|
|
||||||
logWarn: (message, details) => logger.warn(message, details),
|
|
||||||
}),
|
|
||||||
loadYomitanExtension: async () => {
|
|
||||||
await loadYomitanExtension();
|
|
||||||
},
|
|
||||||
startJellyfinRemoteSession: async () => {
|
|
||||||
await startJellyfinRemoteSession();
|
|
||||||
},
|
|
||||||
prewarmSubtitleDictionaries: async () => {
|
|
||||||
await prewarmSubtitleDictionaries();
|
|
||||||
},
|
|
||||||
startBackgroundWarmups: () => {
|
|
||||||
startBackgroundWarmups();
|
|
||||||
},
|
|
||||||
texthookerOnlyMode: appState.texthookerOnlyMode,
|
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
|
||||||
appState.backgroundMode
|
|
||||||
? false
|
|
||||||
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
|
||||||
handleInitialArgs: () => handleInitialArgs(),
|
|
||||||
onCriticalConfigErrors: createCriticalConfigErrorHandler({
|
|
||||||
getConfigPath: () => configService.getConfigPath(),
|
|
||||||
failHandlers: {
|
|
||||||
logError: (message) => logger.error(message),
|
|
||||||
showErrorBox: (title, message) => dialog.showErrorBox(title, message),
|
|
||||||
quit: () => app.quit(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
logDebug: (message: string) => {
|
|
||||||
logger.debug(message);
|
|
||||||
},
|
|
||||||
now: () => Date.now(),
|
|
||||||
}),
|
|
||||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
|
||||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
|
||||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
|
||||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
|
||||||
}),
|
}),
|
||||||
});
|
loadYomitanExtension: async () => {
|
||||||
|
await loadYomitanExtension();
|
||||||
|
},
|
||||||
|
startJellyfinRemoteSession: async () => {
|
||||||
|
await startJellyfinRemoteSession();
|
||||||
|
},
|
||||||
|
prewarmSubtitleDictionaries: async () => {
|
||||||
|
await prewarmSubtitleDictionaries();
|
||||||
|
},
|
||||||
|
startBackgroundWarmups: () => {
|
||||||
|
startBackgroundWarmups();
|
||||||
|
},
|
||||||
|
texthookerOnlyMode: appState.texthookerOnlyMode,
|
||||||
|
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||||
|
appState.backgroundMode
|
||||||
|
? false
|
||||||
|
: configDerivedRuntime.shouldAutoInitializeOverlayRuntimeFromConfig(),
|
||||||
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
|
handleInitialArgs: () => handleInitialArgs(),
|
||||||
|
onCriticalConfigErrors: criticalConfigErrorHandler,
|
||||||
|
logDebug: (message: string) => {
|
||||||
|
logger.debug(message);
|
||||||
|
},
|
||||||
|
now: () => Date.now(),
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const appLifecycleRuntimeRunner = createAppLifecycleRuntimeRunner(
|
||||||
|
createBuildAppLifecycleRuntimeRunnerMainDepsHandler({
|
||||||
|
app,
|
||||||
|
platform: process.platform,
|
||||||
|
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||||
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
|
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||||
|
handleCliCommand(nextArgs, source),
|
||||||
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
|
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||||
|
onReady: appReadyRuntimeRunner,
|
||||||
|
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||||
|
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||||
|
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||||
|
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const buildStartupBootstrapRuntimeFactoryDepsHandler =
|
||||||
|
createBuildStartupBootstrapRuntimeFactoryDepsHandler(
|
||||||
|
createBuildStartupBootstrapMainDepsHandler({
|
||||||
|
argv: process.argv,
|
||||||
|
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||||
|
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||||
|
setLogLevel(level, source);
|
||||||
|
},
|
||||||
|
forceX11Backend: (args: CliArgs) => {
|
||||||
|
forceX11Backend(args);
|
||||||
|
},
|
||||||
|
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||||
|
enforceUnsupportedWaylandMode(args);
|
||||||
|
},
|
||||||
|
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||||
|
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||||
|
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||||
|
configDir: CONFIG_DIR,
|
||||||
|
defaultConfig: DEFAULT_CONFIG,
|
||||||
|
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||||
|
generateDefaultConfigFile: (
|
||||||
|
args: CliArgs,
|
||||||
|
options: {
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: unknown;
|
||||||
|
generateTemplate: (config: unknown) => string;
|
||||||
|
},
|
||||||
|
) => generateDefaultConfigFile(args, options),
|
||||||
|
setExitCode: (code) => {
|
||||||
|
process.exitCode = code;
|
||||||
|
},
|
||||||
|
quitApp: () => app.quit(),
|
||||||
|
logGenerateConfigError: (message) => logger.error(message),
|
||||||
|
startAppLifecycle: appLifecycleRuntimeRunner,
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
const startupState = runStartupBootstrapRuntime(
|
const startupState = runStartupBootstrapRuntime(
|
||||||
createStartupBootstrapRuntimeDeps(buildStartupBootstrapRuntimeFactoryDepsHandler()),
|
createStartupBootstrapRuntimeDeps(buildStartupBootstrapRuntimeFactoryDepsHandler()),
|
||||||
@@ -1718,16 +1798,20 @@ applyStartupState(appState, startupState);
|
|||||||
void refreshAnilistClientSecretState({ force: true });
|
void refreshAnilistClientSecretState({ force: true });
|
||||||
anilistStateRuntime.refreshRetryQueueState();
|
anilistStateRuntime.refreshRetryQueueState();
|
||||||
|
|
||||||
function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'): void {
|
const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler(
|
||||||
createHandleTexthookerOnlyModeTransitionHandler({
|
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({
|
||||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||||
setTexthookerOnlyMode: (enabled) => {
|
setTexthookerOnlyMode: (enabled) => {
|
||||||
appState.texthookerOnlyMode = enabled;
|
appState.texthookerOnlyMode = enabled;
|
||||||
},
|
},
|
||||||
commandNeedsOverlayRuntime: (inputArgs) => commandNeedsOverlayRuntime(inputArgs),
|
commandNeedsOverlayRuntime: (inputArgs) => commandNeedsOverlayRuntime(inputArgs),
|
||||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message: string) => logger.info(message),
|
||||||
})(args);
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'): void {
|
||||||
|
handleTexthookerOnlyModeTransitionHandler(args);
|
||||||
|
|
||||||
const cliContext = createCliCommandContext(buildCliCommandContextDepsHandler());
|
const cliContext = createCliCommandContext(buildCliCommandContextDepsHandler());
|
||||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
||||||
@@ -1833,59 +1917,62 @@ function updateMpvSubtitleRenderMetrics(patch: Partial<MpvSubtitleRenderMetrics>
|
|||||||
updateMpvSubtitleRenderMetricsRuntime(patch);
|
updateMpvSubtitleRenderMetricsRuntime(patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler({
|
||||||
|
getYomitanExt: () => appState.yomitanExt,
|
||||||
|
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||||
|
setYomitanParserWindow: (window) => {
|
||||||
|
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||||
|
},
|
||||||
|
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
||||||
|
setYomitanParserReadyPromise: (promise) => {
|
||||||
|
appState.yomitanParserReadyPromise = promise;
|
||||||
|
},
|
||||||
|
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
||||||
|
setYomitanParserInitPromise: (promise) => {
|
||||||
|
appState.yomitanParserInitPromise = promise;
|
||||||
|
},
|
||||||
|
isKnownWord: (text) => Boolean(appState.ankiIntegration?.isKnownWord(text)),
|
||||||
|
recordLookup: (hit) => {
|
||||||
|
appState.immersionTracker?.recordLookup(hit);
|
||||||
|
},
|
||||||
|
getKnownWordMatchMode: () =>
|
||||||
|
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
||||||
|
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
||||||
|
getMinSentenceWordsForNPlusOne: () => getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
||||||
|
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
||||||
|
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||||
|
getFrequencyDictionaryEnabled: () => getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||||
|
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
||||||
|
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||||
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMecabTokenizerAndCheckHandler = createCreateMecabTokenizerAndCheckMainHandler({
|
||||||
|
getMecabTokenizer: () => appState.mecabTokenizer,
|
||||||
|
setMecabTokenizer: (tokenizer) => {
|
||||||
|
appState.mecabTokenizer = tokenizer;
|
||||||
|
},
|
||||||
|
createMecabTokenizer: () => new MecabTokenizer(),
|
||||||
|
checkAvailability: async (tokenizer) => tokenizer.checkAvailability(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const prewarmSubtitleDictionariesHandler = createPrewarmSubtitleDictionariesMainHandler({
|
||||||
|
ensureJlptDictionaryLookup: () => jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
||||||
|
ensureFrequencyDictionaryLookup: () => frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
||||||
|
});
|
||||||
|
|
||||||
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
async function tokenizeSubtitle(text: string): Promise<SubtitleData> {
|
||||||
await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
|
await jlptDictionaryRuntime.ensureJlptDictionaryLookup();
|
||||||
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
|
await frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup();
|
||||||
return tokenizeSubtitleCore(
|
return tokenizeSubtitleCore(text, createTokenizerDepsRuntime(buildTokenizerDepsHandler()));
|
||||||
text,
|
|
||||||
createTokenizerDepsRuntime({
|
|
||||||
getYomitanExt: () => appState.yomitanExt,
|
|
||||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
|
||||||
setYomitanParserWindow: (window) => {
|
|
||||||
appState.yomitanParserWindow = window;
|
|
||||||
},
|
|
||||||
getYomitanParserReadyPromise: () => appState.yomitanParserReadyPromise,
|
|
||||||
setYomitanParserReadyPromise: (promise) => {
|
|
||||||
appState.yomitanParserReadyPromise = promise;
|
|
||||||
},
|
|
||||||
getYomitanParserInitPromise: () => appState.yomitanParserInitPromise,
|
|
||||||
setYomitanParserInitPromise: (promise) => {
|
|
||||||
appState.yomitanParserInitPromise = promise;
|
|
||||||
},
|
|
||||||
isKnownWord: (text) =>
|
|
||||||
(() => {
|
|
||||||
const hit = Boolean(appState.ankiIntegration?.isKnownWord(text));
|
|
||||||
appState.immersionTracker?.recordLookup(hit);
|
|
||||||
return hit;
|
|
||||||
})(),
|
|
||||||
getKnownWordMatchMode: () =>
|
|
||||||
appState.ankiIntegration?.getKnownWordMatchMode() ??
|
|
||||||
getResolvedConfig().ankiConnect.nPlusOne.matchMode,
|
|
||||||
getMinSentenceWordsForNPlusOne: () =>
|
|
||||||
getResolvedConfig().ankiConnect.nPlusOne.minSentenceWords,
|
|
||||||
getJlptLevel: (text) => appState.jlptLevelLookup(text),
|
|
||||||
getJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
|
||||||
getFrequencyDictionaryEnabled: () =>
|
|
||||||
getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
|
||||||
getFrequencyRank: (text) => appState.frequencyRankLookup(text),
|
|
||||||
getYomitanGroupDebugEnabled: () => appState.overlayDebugVisualizationEnabled,
|
|
||||||
getMecabTokenizer: () => appState.mecabTokenizer,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createMecabTokenizerAndCheck(): Promise<void> {
|
async function createMecabTokenizerAndCheck(): Promise<void> {
|
||||||
if (!appState.mecabTokenizer) {
|
await createMecabTokenizerAndCheckHandler();
|
||||||
appState.mecabTokenizer = new MecabTokenizer();
|
|
||||||
}
|
|
||||||
await appState.mecabTokenizer.checkAvailability();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prewarmSubtitleDictionaries(): Promise<void> {
|
async function prewarmSubtitleDictionaries(): Promise<void> {
|
||||||
await Promise.all([
|
await prewarmSubtitleDictionariesHandler();
|
||||||
jlptDictionaryRuntime.ensureJlptDictionaryLookup(),
|
|
||||||
frequencyDictionaryRuntime.ensureFrequencyDictionaryLookup(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler({
|
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskHandler({
|
||||||
@@ -2020,20 +2107,22 @@ function getConfiguredShortcuts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cycleSecondarySubMode(): void {
|
function cycleSecondarySubMode(): void {
|
||||||
cycleSecondarySubModeCore({
|
cycleSecondarySubModeCore(
|
||||||
getSecondarySubMode: () => appState.secondarySubMode,
|
createBuildCycleSecondarySubModeMainDepsHandler({
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
getSecondarySubMode: () => appState.secondarySubMode,
|
||||||
appState.secondarySubMode = mode;
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
},
|
appState.secondarySubMode = mode;
|
||||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
},
|
||||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||||
appState.lastSecondarySubToggleAtMs = timestampMs;
|
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||||
},
|
appState.lastSecondarySubToggleAtMs = timestampMs;
|
||||||
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
|
},
|
||||||
broadcastToOverlayWindows('secondary-subtitle:mode', mode);
|
broadcastToOverlayWindows: (channel, mode) => {
|
||||||
},
|
broadcastToOverlayWindows(channel, mode);
|
||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
},
|
||||||
});
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
|
})(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appendToMpvLogHandler = createAppendToMpvLogHandler({
|
const appendToMpvLogHandler = createAppendToMpvLogHandler({
|
||||||
|
|||||||
71
src/main/runtime/app-ready-main-deps.test.ts
Normal file
71
src/main/runtime/app-ready-main-deps.test.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildAppReadyRuntimeMainDepsHandler } from './app-ready-main-deps';
|
||||||
|
|
||||||
|
test('app-ready main deps builder returns mapped app-ready runtime deps', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const onReady = createBuildAppReadyRuntimeMainDepsHandler({
|
||||||
|
loadSubtitlePosition: () => calls.push('load-subtitle-position'),
|
||||||
|
resolveKeybindings: () => calls.push('resolve-keybindings'),
|
||||||
|
createMpvClient: () => calls.push('create-mpv-client'),
|
||||||
|
reloadConfig: () => calls.push('reload-config'),
|
||||||
|
getResolvedConfig: () => ({ websocket: {} }),
|
||||||
|
getConfigWarnings: () => [],
|
||||||
|
logConfigWarning: () => calls.push('log-config-warning'),
|
||||||
|
initRuntimeOptionsManager: () => calls.push('init-runtime-options'),
|
||||||
|
setSecondarySubMode: () => calls.push('set-secondary-sub-mode'),
|
||||||
|
defaultSecondarySubMode: 'hover',
|
||||||
|
defaultWebsocketPort: 5174,
|
||||||
|
hasMpvWebsocketPlugin: () => false,
|
||||||
|
startSubtitleWebsocket: () => calls.push('start-ws'),
|
||||||
|
log: () => calls.push('log'),
|
||||||
|
setLogLevel: () => calls.push('set-log-level'),
|
||||||
|
createMecabTokenizerAndCheck: async () => {
|
||||||
|
calls.push('create-mecab');
|
||||||
|
},
|
||||||
|
createSubtitleTimingTracker: () => calls.push('create-subtitle-tracker'),
|
||||||
|
createImmersionTracker: () => calls.push('create-immersion'),
|
||||||
|
startJellyfinRemoteSession: async () => {
|
||||||
|
calls.push('start-jellyfin');
|
||||||
|
},
|
||||||
|
loadYomitanExtension: async () => {
|
||||||
|
calls.push('load-yomitan');
|
||||||
|
},
|
||||||
|
prewarmSubtitleDictionaries: async () => {
|
||||||
|
calls.push('prewarm-dicts');
|
||||||
|
},
|
||||||
|
startBackgroundWarmups: () => calls.push('start-warmups'),
|
||||||
|
texthookerOnlyMode: false,
|
||||||
|
shouldAutoInitializeOverlayRuntimeFromConfig: () => true,
|
||||||
|
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||||
|
handleInitialArgs: () => calls.push('handle-initial-args'),
|
||||||
|
onCriticalConfigErrors: () => {
|
||||||
|
throw new Error('should not call');
|
||||||
|
},
|
||||||
|
logDebug: () => calls.push('debug'),
|
||||||
|
now: () => 123,
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(onReady.defaultSecondarySubMode, 'hover');
|
||||||
|
assert.equal(onReady.defaultWebsocketPort, 5174);
|
||||||
|
assert.equal(onReady.texthookerOnlyMode, false);
|
||||||
|
assert.equal(onReady.shouldAutoInitializeOverlayRuntimeFromConfig(), true);
|
||||||
|
assert.equal(onReady.now?.(), 123);
|
||||||
|
onReady.loadSubtitlePosition();
|
||||||
|
onReady.resolveKeybindings();
|
||||||
|
onReady.createMpvClient();
|
||||||
|
await onReady.createMecabTokenizerAndCheck();
|
||||||
|
await onReady.loadYomitanExtension();
|
||||||
|
await onReady.prewarmSubtitleDictionaries?.();
|
||||||
|
onReady.startBackgroundWarmups();
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'load-subtitle-position',
|
||||||
|
'resolve-keybindings',
|
||||||
|
'create-mpv-client',
|
||||||
|
'create-mecab',
|
||||||
|
'load-yomitan',
|
||||||
|
'prewarm-dicts',
|
||||||
|
'start-warmups',
|
||||||
|
]);
|
||||||
|
});
|
||||||
38
src/main/runtime/app-ready-main-deps.ts
Normal file
38
src/main/runtime/app-ready-main-deps.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
|
||||||
|
|
||||||
|
export function createBuildAppReadyRuntimeMainDepsHandler(
|
||||||
|
deps: AppReadyRuntimeDepsFactoryInput,
|
||||||
|
) {
|
||||||
|
return (): AppReadyRuntimeDepsFactoryInput => ({
|
||||||
|
loadSubtitlePosition: deps.loadSubtitlePosition,
|
||||||
|
resolveKeybindings: deps.resolveKeybindings,
|
||||||
|
createMpvClient: deps.createMpvClient,
|
||||||
|
reloadConfig: deps.reloadConfig,
|
||||||
|
getResolvedConfig: deps.getResolvedConfig,
|
||||||
|
getConfigWarnings: deps.getConfigWarnings,
|
||||||
|
logConfigWarning: deps.logConfigWarning,
|
||||||
|
initRuntimeOptionsManager: deps.initRuntimeOptionsManager,
|
||||||
|
setSecondarySubMode: deps.setSecondarySubMode,
|
||||||
|
defaultSecondarySubMode: deps.defaultSecondarySubMode,
|
||||||
|
defaultWebsocketPort: deps.defaultWebsocketPort,
|
||||||
|
hasMpvWebsocketPlugin: deps.hasMpvWebsocketPlugin,
|
||||||
|
startSubtitleWebsocket: deps.startSubtitleWebsocket,
|
||||||
|
log: deps.log,
|
||||||
|
setLogLevel: deps.setLogLevel,
|
||||||
|
createMecabTokenizerAndCheck: deps.createMecabTokenizerAndCheck,
|
||||||
|
createSubtitleTimingTracker: deps.createSubtitleTimingTracker,
|
||||||
|
createImmersionTracker: deps.createImmersionTracker,
|
||||||
|
startJellyfinRemoteSession: deps.startJellyfinRemoteSession,
|
||||||
|
loadYomitanExtension: deps.loadYomitanExtension,
|
||||||
|
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
||||||
|
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||||
|
texthookerOnlyMode: deps.texthookerOnlyMode,
|
||||||
|
shouldAutoInitializeOverlayRuntimeFromConfig:
|
||||||
|
deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||||
|
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||||
|
handleInitialArgs: deps.handleInitialArgs,
|
||||||
|
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||||
|
logDebug: deps.logDebug,
|
||||||
|
now: deps.now,
|
||||||
|
});
|
||||||
|
}
|
||||||
21
src/main/runtime/cli-command-prechecks-main-deps.test.ts
Normal file
21
src/main/runtime/cli-command-prechecks-main-deps.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler } from './cli-command-prechecks-main-deps';
|
||||||
|
|
||||||
|
test('cli prechecks main deps builder maps transition handlers', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler({
|
||||||
|
isTexthookerOnlyMode: () => true,
|
||||||
|
setTexthookerOnlyMode: (enabled) => calls.push(`set:${enabled}`),
|
||||||
|
commandNeedsOverlayRuntime: () => true,
|
||||||
|
startBackgroundWarmups: () => calls.push('warmups'),
|
||||||
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isTexthookerOnlyMode(), true);
|
||||||
|
assert.equal(deps.commandNeedsOverlayRuntime({} as never), true);
|
||||||
|
deps.setTexthookerOnlyMode(false);
|
||||||
|
deps.startBackgroundWarmups();
|
||||||
|
deps.logInfo('x');
|
||||||
|
assert.deepEqual(calls, ['set:false', 'warmups', 'info:x']);
|
||||||
|
});
|
||||||
17
src/main/runtime/cli-command-prechecks-main-deps.ts
Normal file
17
src/main/runtime/cli-command-prechecks-main-deps.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { CliArgs } from '../../cli/args';
|
||||||
|
|
||||||
|
export function createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(deps: {
|
||||||
|
isTexthookerOnlyMode: () => boolean;
|
||||||
|
setTexthookerOnlyMode: (enabled: boolean) => void;
|
||||||
|
commandNeedsOverlayRuntime: (args: CliArgs) => boolean;
|
||||||
|
startBackgroundWarmups: () => void;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
isTexthookerOnlyMode: () => deps.isTexthookerOnlyMode(),
|
||||||
|
setTexthookerOnlyMode: (enabled: boolean) => deps.setTexthookerOnlyMode(enabled),
|
||||||
|
commandNeedsOverlayRuntime: (args: CliArgs) => deps.commandNeedsOverlayRuntime(args),
|
||||||
|
startBackgroundWarmups: () => deps.startBackgroundWarmups(),
|
||||||
|
logInfo: (message: string) => deps.logInfo(message),
|
||||||
|
});
|
||||||
|
}
|
||||||
56
src/main/runtime/field-grouping-resolver.test.ts
Normal file
56
src/main/runtime/field-grouping-resolver.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createGetFieldGroupingResolverHandler,
|
||||||
|
createSetFieldGroupingResolverHandler,
|
||||||
|
} from './field-grouping-resolver';
|
||||||
|
|
||||||
|
test('get field grouping resolver returns current resolver', () => {
|
||||||
|
const resolver = () => undefined;
|
||||||
|
const getResolver = createGetFieldGroupingResolverHandler({
|
||||||
|
getResolver: () => resolver,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(getResolver(), resolver);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set field grouping resolver clears resolver when null is provided', () => {
|
||||||
|
let current: ((choice: unknown) => void) | null = () => undefined;
|
||||||
|
const setResolver = createSetFieldGroupingResolverHandler({
|
||||||
|
setResolver: (resolver) => {
|
||||||
|
current = resolver as never;
|
||||||
|
},
|
||||||
|
nextSequence: () => 1,
|
||||||
|
getSequence: () => 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResolver(null);
|
||||||
|
assert.equal(current, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set field grouping resolver wraps resolver and ignores stale sequence', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let current: ((choice: unknown) => void) | null = null;
|
||||||
|
let sequence = 0;
|
||||||
|
|
||||||
|
const setResolver = createSetFieldGroupingResolverHandler({
|
||||||
|
setResolver: (resolver) => {
|
||||||
|
current = resolver as never;
|
||||||
|
},
|
||||||
|
nextSequence: () => {
|
||||||
|
sequence += 1;
|
||||||
|
return sequence;
|
||||||
|
},
|
||||||
|
getSequence: () => sequence,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResolver((choice) => calls.push(`new:${choice}`));
|
||||||
|
const firstWrapped = current!;
|
||||||
|
setResolver((choice) => calls.push(`latest:${choice}`));
|
||||||
|
const latestWrapped = current!;
|
||||||
|
|
||||||
|
firstWrapped('A');
|
||||||
|
latestWrapped('B');
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['latest:B']);
|
||||||
|
});
|
||||||
29
src/main/runtime/field-grouping-resolver.ts
Normal file
29
src/main/runtime/field-grouping-resolver.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { KikuFieldGroupingChoice } from '../../types';
|
||||||
|
|
||||||
|
type FieldGroupingResolver = ((choice: KikuFieldGroupingChoice) => void) | null;
|
||||||
|
|
||||||
|
export function createGetFieldGroupingResolverHandler(deps: {
|
||||||
|
getResolver: () => FieldGroupingResolver;
|
||||||
|
}) {
|
||||||
|
return (): FieldGroupingResolver => deps.getResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSetFieldGroupingResolverHandler(deps: {
|
||||||
|
setResolver: (resolver: FieldGroupingResolver) => void;
|
||||||
|
nextSequence: () => number;
|
||||||
|
getSequence: () => number;
|
||||||
|
}) {
|
||||||
|
return (resolver: FieldGroupingResolver): void => {
|
||||||
|
if (!resolver) {
|
||||||
|
deps.setResolver(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sequence = deps.nextSequence();
|
||||||
|
const wrappedResolver = (choice: KikuFieldGroupingChoice): void => {
|
||||||
|
if (sequence !== deps.getSequence()) return;
|
||||||
|
resolver(choice);
|
||||||
|
};
|
||||||
|
deps.setResolver(wrappedResolver);
|
||||||
|
};
|
||||||
|
}
|
||||||
56
src/main/runtime/jellyfin-client-info.test.ts
Normal file
56
src/main/runtime/jellyfin-client-info.test.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createGetJellyfinClientInfoHandler,
|
||||||
|
createGetResolvedJellyfinConfigHandler,
|
||||||
|
} from './jellyfin-client-info';
|
||||||
|
|
||||||
|
test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
|
||||||
|
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
||||||
|
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||||
|
getResolvedConfig: () => ({ jellyfin } as never),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(getConfig(), jellyfin);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin client info resolves defaults when fields are missing', () => {
|
||||||
|
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||||
|
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
||||||
|
getDefaultJellyfinConfig: () =>
|
||||||
|
({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0.0',
|
||||||
|
deviceId: 'default-device',
|
||||||
|
}) as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(getClientInfo(), {
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0.0',
|
||||||
|
deviceId: 'default-device',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin client info keeps explicit config values', () => {
|
||||||
|
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||||
|
getResolvedJellyfinConfig: () =>
|
||||||
|
({
|
||||||
|
clientName: 'Custom',
|
||||||
|
clientVersion: '2.3.4',
|
||||||
|
deviceId: 'custom-device',
|
||||||
|
}) as never,
|
||||||
|
getDefaultJellyfinConfig: () =>
|
||||||
|
({
|
||||||
|
clientName: 'SubMiner',
|
||||||
|
clientVersion: '1.0.0',
|
||||||
|
deviceId: 'default-device',
|
||||||
|
}) as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(getClientInfo(), {
|
||||||
|
clientName: 'Custom',
|
||||||
|
clientVersion: '2.3.4',
|
||||||
|
deviceId: 'custom-device',
|
||||||
|
});
|
||||||
|
});
|
||||||
33
src/main/runtime/jellyfin-client-info.ts
Normal file
33
src/main/runtime/jellyfin-client-info.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export function createGetResolvedJellyfinConfigHandler(deps: {
|
||||||
|
getResolvedConfig: () => { jellyfin: unknown };
|
||||||
|
}) {
|
||||||
|
return () => deps.getResolvedConfig().jellyfin as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGetJellyfinClientInfoHandler(deps: {
|
||||||
|
getResolvedJellyfinConfig: () => {
|
||||||
|
clientName?: string;
|
||||||
|
clientVersion?: string;
|
||||||
|
deviceId?: string;
|
||||||
|
};
|
||||||
|
getDefaultJellyfinConfig: () => {
|
||||||
|
clientName?: string;
|
||||||
|
clientVersion?: string;
|
||||||
|
deviceId?: string;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
config = deps.getResolvedJellyfinConfig(),
|
||||||
|
): {
|
||||||
|
clientName: string;
|
||||||
|
clientVersion: string;
|
||||||
|
deviceId: string;
|
||||||
|
} => {
|
||||||
|
const defaults = deps.getDefaultJellyfinConfig();
|
||||||
|
return {
|
||||||
|
clientName: config.clientName || defaults.clientName || '',
|
||||||
|
clientVersion: config.clientVersion || defaults.clientVersion || '',
|
||||||
|
deviceId: config.deviceId || defaults.deviceId || '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
32
src/main/runtime/mpv-jellyfin-defaults.test.ts
Normal file
32
src/main/runtime/mpv-jellyfin-defaults.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createApplyJellyfinMpvDefaultsHandler,
|
||||||
|
createGetDefaultSocketPathHandler,
|
||||||
|
} from './mpv-jellyfin-defaults';
|
||||||
|
|
||||||
|
test('apply jellyfin mpv defaults sends expected property commands', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const applyDefaults = createApplyJellyfinMpvDefaultsHandler({
|
||||||
|
sendMpvCommandRuntime: (_client, command) => calls.push(command.join(':')),
|
||||||
|
jellyfinLangPref: 'ja,jp',
|
||||||
|
});
|
||||||
|
|
||||||
|
applyDefaults({});
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'set_property:sub-auto:fuzzy',
|
||||||
|
'set_property:aid:auto',
|
||||||
|
'set_property:sid:auto',
|
||||||
|
'set_property:secondary-sid:auto',
|
||||||
|
'set_property:secondary-sub-visibility:no',
|
||||||
|
'set_property:alang:ja,jp',
|
||||||
|
'set_property:slang:ja,jp',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get default socket path returns platform specific value', () => {
|
||||||
|
const getWindowsPath = createGetDefaultSocketPathHandler({ platform: 'win32' });
|
||||||
|
const getUnixPath = createGetDefaultSocketPathHandler({ platform: 'darwin' });
|
||||||
|
assert.equal(getWindowsPath(), '\\\\.\\pipe\\subminer-socket');
|
||||||
|
assert.equal(getUnixPath(), '/tmp/subminer-socket');
|
||||||
|
});
|
||||||
30
src/main/runtime/mpv-jellyfin-defaults.ts
Normal file
30
src/main/runtime/mpv-jellyfin-defaults.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
type MpvClientLike = unknown;
|
||||||
|
|
||||||
|
export function createApplyJellyfinMpvDefaultsHandler(deps: {
|
||||||
|
sendMpvCommandRuntime: (
|
||||||
|
client: MpvClientLike,
|
||||||
|
command: [string, string, string],
|
||||||
|
) => void;
|
||||||
|
jellyfinLangPref: string;
|
||||||
|
}) {
|
||||||
|
return (client: MpvClientLike): void => {
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'secondary-sid', 'auto']);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'secondary-sub-visibility', 'no']);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'alang', deps.jellyfinLangPref]);
|
||||||
|
deps.sendMpvCommandRuntime(client, ['set_property', 'slang', deps.jellyfinLangPref]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGetDefaultSocketPathHandler(deps: {
|
||||||
|
platform: string;
|
||||||
|
}) {
|
||||||
|
return (): string => {
|
||||||
|
if (deps.platform === 'win32') {
|
||||||
|
return '\\\\.\\pipe\\subminer-socket';
|
||||||
|
}
|
||||||
|
return '/tmp/subminer-socket';
|
||||||
|
};
|
||||||
|
}
|
||||||
134
src/main/runtime/overlay-runtime-main-actions.test.ts
Normal file
134
src/main/runtime/overlay-runtime-main-actions.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createBroadcastRuntimeOptionsChangedHandler,
|
||||||
|
createGetRuntimeOptionsStateHandler,
|
||||||
|
createOpenRuntimeOptionsPaletteHandler,
|
||||||
|
createRestorePreviousSecondarySubVisibilityHandler,
|
||||||
|
createSendToActiveOverlayWindowHandler,
|
||||||
|
createSetOverlayDebugVisualizationEnabledHandler,
|
||||||
|
} from './overlay-runtime-main-actions';
|
||||||
|
|
||||||
|
test('runtime options state handler returns empty list without manager', () => {
|
||||||
|
const getState = createGetRuntimeOptionsStateHandler({
|
||||||
|
getRuntimeOptionsManager: () => null,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getState(), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runtime options state handler returns list from manager', () => {
|
||||||
|
const getState = createGetRuntimeOptionsStateHandler({
|
||||||
|
getRuntimeOptionsManager: () =>
|
||||||
|
({
|
||||||
|
listOptions: () => [
|
||||||
|
{
|
||||||
|
id: 'anki.autoUpdateNewCards',
|
||||||
|
label: 'X',
|
||||||
|
scope: 'ankiConnect',
|
||||||
|
valueType: 'boolean',
|
||||||
|
value: true,
|
||||||
|
allowedValues: [true, false],
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}) as never,
|
||||||
|
});
|
||||||
|
assert.deepEqual(getState(), [
|
||||||
|
{
|
||||||
|
id: 'anki.autoUpdateNewCards',
|
||||||
|
label: 'X',
|
||||||
|
scope: 'ankiConnect',
|
||||||
|
valueType: 'boolean',
|
||||||
|
value: true,
|
||||||
|
allowedValues: [true, false],
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore previous secondary subtitle visibility no-ops without connected mpv client', () => {
|
||||||
|
let restored = false;
|
||||||
|
const restore = createRestorePreviousSecondarySubVisibilityHandler({
|
||||||
|
getMpvClient: () => ({ connected: false, restorePreviousSecondarySubVisibility: () => (restored = true) }),
|
||||||
|
});
|
||||||
|
restore();
|
||||||
|
assert.equal(restored, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore previous secondary subtitle visibility calls runtime when connected', () => {
|
||||||
|
let restored = false;
|
||||||
|
const restore = createRestorePreviousSecondarySubVisibilityHandler({
|
||||||
|
getMpvClient: () => ({ connected: true, restorePreviousSecondarySubVisibility: () => (restored = true) }),
|
||||||
|
});
|
||||||
|
restore();
|
||||||
|
assert.equal(restored, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('broadcast runtime options changed passes through state getter and broadcaster', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const broadcast = createBroadcastRuntimeOptionsChangedHandler({
|
||||||
|
broadcastRuntimeOptionsChangedRuntime: (getState, emit) => {
|
||||||
|
calls.push(`state:${JSON.stringify(getState())}`);
|
||||||
|
emit('runtime-options:changed', { id: 1 });
|
||||||
|
},
|
||||||
|
getRuntimeOptionsState: () => [
|
||||||
|
{
|
||||||
|
id: 'anki.autoUpdateNewCards',
|
||||||
|
label: 'X',
|
||||||
|
scope: 'ankiConnect',
|
||||||
|
valueType: 'boolean',
|
||||||
|
value: true,
|
||||||
|
allowedValues: [true, false],
|
||||||
|
requiresRestart: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
broadcastToOverlayWindows: (channel, payload) => calls.push(`emit:${channel}:${JSON.stringify(payload)}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcast();
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'state:[{"id":"anki.autoUpdateNewCards","label":"X","scope":"ankiConnect","valueType":"boolean","value":true,"allowedValues":[true,false],"requiresRestart":false}]',
|
||||||
|
'emit:runtime-options:changed:{"id":1}',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('send to active overlay window delegates to runtime sender', () => {
|
||||||
|
const send = createSendToActiveOverlayWindowHandler({
|
||||||
|
sendToActiveOverlayWindowRuntime: (channel, payload) => channel === 'ok' && payload === 1,
|
||||||
|
});
|
||||||
|
assert.equal(send('ok', 1), true);
|
||||||
|
assert.equal(send('no', 1), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set overlay debug visualization enabled delegates with current state and broadcast', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let current = false;
|
||||||
|
const setEnabled = createSetOverlayDebugVisualizationEnabledHandler({
|
||||||
|
setOverlayDebugVisualizationEnabledRuntime: (curr, next, setCurrent, broadcast) => {
|
||||||
|
calls.push(`runtime:${curr}->${next}`);
|
||||||
|
setCurrent(next);
|
||||||
|
broadcast('overlay-debug:set', next);
|
||||||
|
},
|
||||||
|
getCurrentEnabled: () => current,
|
||||||
|
setCurrentEnabled: (enabled) => {
|
||||||
|
current = enabled;
|
||||||
|
calls.push(`set:${enabled}`);
|
||||||
|
},
|
||||||
|
broadcastToOverlayWindows: (channel, value) => calls.push(`emit:${channel}:${value}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
setEnabled(true);
|
||||||
|
assert.equal(current, true);
|
||||||
|
assert.deepEqual(calls, ['runtime:false->true', 'set:true', 'emit:overlay-debug:set:true']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('open runtime options palette handler delegates to runtime', () => {
|
||||||
|
let opened = false;
|
||||||
|
const open = createOpenRuntimeOptionsPaletteHandler({
|
||||||
|
openRuntimeOptionsPaletteRuntime: () => {
|
||||||
|
opened = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
open();
|
||||||
|
assert.equal(opened, true);
|
||||||
|
});
|
||||||
90
src/main/runtime/overlay-runtime-main-actions.ts
Normal file
90
src/main/runtime/overlay-runtime-main-actions.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import type { RuntimeOptionState } from '../../types';
|
||||||
|
import type { OverlayHostedModal } from '../overlay-runtime';
|
||||||
|
|
||||||
|
type RuntimeOptionsManagerLike = {
|
||||||
|
listOptions: () => RuntimeOptionState[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type MpvClientLike = {
|
||||||
|
connected: boolean;
|
||||||
|
restorePreviousSecondarySubVisibility: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createGetRuntimeOptionsStateHandler(deps: {
|
||||||
|
getRuntimeOptionsManager: () => RuntimeOptionsManagerLike | null;
|
||||||
|
}) {
|
||||||
|
return (): RuntimeOptionState[] => {
|
||||||
|
const manager = deps.getRuntimeOptionsManager();
|
||||||
|
if (!manager) return [];
|
||||||
|
return manager.listOptions();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRestorePreviousSecondarySubVisibilityHandler(deps: {
|
||||||
|
getMpvClient: () => MpvClientLike | null;
|
||||||
|
}) {
|
||||||
|
return (): void => {
|
||||||
|
const client = deps.getMpvClient();
|
||||||
|
if (!client || !client.connected) return;
|
||||||
|
client.restorePreviousSecondarySubVisibility();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBroadcastRuntimeOptionsChangedHandler(deps: {
|
||||||
|
broadcastRuntimeOptionsChangedRuntime: (
|
||||||
|
getRuntimeOptionsState: () => RuntimeOptionState[],
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||||
|
) => void;
|
||||||
|
getRuntimeOptionsState: () => RuntimeOptionState[];
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||||
|
}) {
|
||||||
|
return (): void => {
|
||||||
|
deps.broadcastRuntimeOptionsChangedRuntime(
|
||||||
|
() => deps.getRuntimeOptionsState(),
|
||||||
|
(channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSendToActiveOverlayWindowHandler(deps: {
|
||||||
|
sendToActiveOverlayWindowRuntime: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
|
) => boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
|
||||||
|
): boolean => deps.sendToActiveOverlayWindowRuntime(channel, payload, runtimeOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSetOverlayDebugVisualizationEnabledHandler(deps: {
|
||||||
|
setOverlayDebugVisualizationEnabledRuntime: (
|
||||||
|
currentEnabled: boolean,
|
||||||
|
nextEnabled: boolean,
|
||||||
|
setCurrentEnabled: (enabled: boolean) => void,
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
||||||
|
) => void;
|
||||||
|
getCurrentEnabled: () => boolean;
|
||||||
|
setCurrentEnabled: (enabled: boolean) => void;
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||||
|
}) {
|
||||||
|
return (enabled: boolean): void => {
|
||||||
|
deps.setOverlayDebugVisualizationEnabledRuntime(
|
||||||
|
deps.getCurrentEnabled(),
|
||||||
|
enabled,
|
||||||
|
(next) => deps.setCurrentEnabled(next),
|
||||||
|
(channel, ...args) => deps.broadcastToOverlayWindows(channel, ...args),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOpenRuntimeOptionsPaletteHandler(deps: {
|
||||||
|
openRuntimeOptionsPaletteRuntime: () => void;
|
||||||
|
}) {
|
||||||
|
return (): void => {
|
||||||
|
deps.openRuntimeOptionsPaletteRuntime();
|
||||||
|
};
|
||||||
|
}
|
||||||
39
src/main/runtime/secondary-sub-mode-main-deps.test.ts
Normal file
39
src/main/runtime/secondary-sub-mode-main-deps.test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildCycleSecondarySubModeMainDepsHandler } from './secondary-sub-mode-main-deps';
|
||||||
|
import type { SecondarySubMode } from '../../types';
|
||||||
|
|
||||||
|
test('cycle secondary sub mode main deps builder maps state and broadcasts with channel', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let mode: SecondarySubMode = 'hover';
|
||||||
|
let lastToggleAt = 100;
|
||||||
|
const deps = createBuildCycleSecondarySubModeMainDepsHandler({
|
||||||
|
getSecondarySubMode: () => mode,
|
||||||
|
setSecondarySubMode: (nextMode) => {
|
||||||
|
mode = nextMode;
|
||||||
|
calls.push(`set-mode:${nextMode}`);
|
||||||
|
},
|
||||||
|
getLastSecondarySubToggleAtMs: () => lastToggleAt,
|
||||||
|
setLastSecondarySubToggleAtMs: (timestampMs) => {
|
||||||
|
lastToggleAt = timestampMs;
|
||||||
|
calls.push(`set-ts:${timestampMs}`);
|
||||||
|
},
|
||||||
|
broadcastToOverlayWindows: (channel, nextMode) => calls.push(`broadcast:${channel}:${nextMode}`),
|
||||||
|
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.getSecondarySubMode(), 'hover');
|
||||||
|
assert.equal(deps.getLastSecondarySubToggleAtMs(), 100);
|
||||||
|
deps.setSecondarySubMode('visible');
|
||||||
|
deps.setLastSecondarySubToggleAtMs(200);
|
||||||
|
deps.broadcastSecondarySubMode('visible');
|
||||||
|
deps.showMpvOsd('Secondary subtitle: visible');
|
||||||
|
assert.equal(mode, 'visible');
|
||||||
|
assert.equal(lastToggleAt, 200);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'set-mode:visible',
|
||||||
|
'set-ts:200',
|
||||||
|
'broadcast:secondary-subtitle:mode:visible',
|
||||||
|
'osd:Secondary subtitle: visible',
|
||||||
|
]);
|
||||||
|
});
|
||||||
21
src/main/runtime/secondary-sub-mode-main-deps.ts
Normal file
21
src/main/runtime/secondary-sub-mode-main-deps.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SecondarySubMode } from '../../types';
|
||||||
|
|
||||||
|
export function createBuildCycleSecondarySubModeMainDepsHandler(deps: {
|
||||||
|
getSecondarySubMode: () => SecondarySubMode;
|
||||||
|
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||||
|
getLastSecondarySubToggleAtMs: () => number;
|
||||||
|
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
|
||||||
|
broadcastToOverlayWindows: (channel: string, mode: SecondarySubMode) => void;
|
||||||
|
showMpvOsd: (text: string) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
getSecondarySubMode: () => deps.getSecondarySubMode(),
|
||||||
|
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
||||||
|
getLastSecondarySubToggleAtMs: () => deps.getLastSecondarySubToggleAtMs(),
|
||||||
|
setLastSecondarySubToggleAtMs: (timestampMs: number) =>
|
||||||
|
deps.setLastSecondarySubToggleAtMs(timestampMs),
|
||||||
|
broadcastSecondarySubMode: (mode: SecondarySubMode) =>
|
||||||
|
deps.broadcastToOverlayWindows('secondary-subtitle:mode', mode),
|
||||||
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
});
|
||||||
|
}
|
||||||
52
src/main/runtime/startup-bootstrap-main-deps.test.ts
Normal file
52
src/main/runtime/startup-bootstrap-main-deps.test.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildStartupBootstrapMainDepsHandler } from './startup-bootstrap-main-deps';
|
||||||
|
|
||||||
|
test('startup bootstrap main deps builder maps deps and handles generate-config callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let exitCode = 0;
|
||||||
|
const deps = createBuildStartupBootstrapMainDepsHandler({
|
||||||
|
argv: ['node', 'main.js'],
|
||||||
|
parseArgs: () => ({}) as never,
|
||||||
|
setLogLevel: (level) => calls.push(`log:${level}`),
|
||||||
|
forceX11Backend: () => calls.push('force-x11'),
|
||||||
|
enforceUnsupportedWaylandMode: () => calls.push('guard-wayland'),
|
||||||
|
shouldStartApp: () => true,
|
||||||
|
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||||
|
defaultTexthookerPort: 5174,
|
||||||
|
configDir: '/tmp/config',
|
||||||
|
defaultConfig: {} as never,
|
||||||
|
generateConfigTemplate: () => 'template',
|
||||||
|
generateDefaultConfigFile: async () => 0,
|
||||||
|
setExitCode: (code) => {
|
||||||
|
exitCode = code;
|
||||||
|
calls.push(`exit:${code}`);
|
||||||
|
},
|
||||||
|
quitApp: () => calls.push('quit'),
|
||||||
|
logGenerateConfigError: (message) => calls.push(`error:${message}`),
|
||||||
|
startAppLifecycle: () => calls.push('start-lifecycle'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.argv, ['node', 'main.js']);
|
||||||
|
assert.equal(deps.getDefaultSocketPath(), '/tmp/mpv.sock');
|
||||||
|
deps.setLogLevel('debug', 'config');
|
||||||
|
deps.forceX11Backend({} as never);
|
||||||
|
deps.enforceUnsupportedWaylandMode({} as never);
|
||||||
|
deps.startAppLifecycle({} as never);
|
||||||
|
deps.onConfigGenerated(7);
|
||||||
|
assert.equal(exitCode, 7);
|
||||||
|
deps.onGenerateConfigError(new Error('boom'));
|
||||||
|
assert.equal(exitCode, 1);
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'log:debug',
|
||||||
|
'force-x11',
|
||||||
|
'guard-wayland',
|
||||||
|
'start-lifecycle',
|
||||||
|
'exit:7',
|
||||||
|
'quit',
|
||||||
|
'error:Failed to generate config: boom',
|
||||||
|
'exit:1',
|
||||||
|
'quit',
|
||||||
|
]);
|
||||||
|
});
|
||||||
62
src/main/runtime/startup-bootstrap-main-deps.ts
Normal file
62
src/main/runtime/startup-bootstrap-main-deps.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import type { CliArgs } from '../../cli/args';
|
||||||
|
import type { LogLevelSource } from '../../logger';
|
||||||
|
import type { ResolvedConfig } from '../../types';
|
||||||
|
import type { StartupBootstrapRuntimeFactoryDeps } from '../startup';
|
||||||
|
|
||||||
|
export function createBuildStartupBootstrapMainDepsHandler(deps: {
|
||||||
|
argv: string[];
|
||||||
|
parseArgs: (argv: string[]) => CliArgs;
|
||||||
|
setLogLevel: (level: string, source: LogLevelSource) => void;
|
||||||
|
forceX11Backend: (args: CliArgs) => void;
|
||||||
|
enforceUnsupportedWaylandMode: (args: CliArgs) => void;
|
||||||
|
shouldStartApp: (args: CliArgs) => boolean;
|
||||||
|
getDefaultSocketPath: () => string;
|
||||||
|
defaultTexthookerPort: number;
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: ResolvedConfig;
|
||||||
|
generateConfigTemplate: (config: ResolvedConfig) => string;
|
||||||
|
generateDefaultConfigFile: (
|
||||||
|
args: CliArgs,
|
||||||
|
options: {
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: unknown;
|
||||||
|
generateTemplate: (config: unknown) => string;
|
||||||
|
},
|
||||||
|
) => Promise<number>;
|
||||||
|
setExitCode: (code: number) => void;
|
||||||
|
quitApp: () => void;
|
||||||
|
logGenerateConfigError: (message: string) => void;
|
||||||
|
startAppLifecycle: (args: CliArgs) => void;
|
||||||
|
}) {
|
||||||
|
return (): StartupBootstrapRuntimeFactoryDeps => ({
|
||||||
|
argv: deps.argv,
|
||||||
|
parseArgs: (argv: string[]) => deps.parseArgs(argv),
|
||||||
|
setLogLevel: (level: string, source: LogLevelSource) => deps.setLogLevel(level, source),
|
||||||
|
forceX11Backend: (args: CliArgs) => deps.forceX11Backend(args),
|
||||||
|
enforceUnsupportedWaylandMode: (args: CliArgs) => deps.enforceUnsupportedWaylandMode(args),
|
||||||
|
shouldStartApp: (args: CliArgs) => deps.shouldStartApp(args),
|
||||||
|
getDefaultSocketPath: () => deps.getDefaultSocketPath(),
|
||||||
|
defaultTexthookerPort: deps.defaultTexthookerPort,
|
||||||
|
configDir: deps.configDir,
|
||||||
|
defaultConfig: deps.defaultConfig,
|
||||||
|
generateConfigTemplate: (config: ResolvedConfig) => deps.generateConfigTemplate(config),
|
||||||
|
generateDefaultConfigFile: (
|
||||||
|
args: CliArgs,
|
||||||
|
options: {
|
||||||
|
configDir: string;
|
||||||
|
defaultConfig: unknown;
|
||||||
|
generateTemplate: (config: unknown) => string;
|
||||||
|
},
|
||||||
|
) => deps.generateDefaultConfigFile(args, options),
|
||||||
|
onConfigGenerated: (exitCode: number) => {
|
||||||
|
deps.setExitCode(exitCode);
|
||||||
|
deps.quitApp();
|
||||||
|
},
|
||||||
|
onGenerateConfigError: (error: Error) => {
|
||||||
|
deps.logGenerateConfigError(`Failed to generate config: ${error.message}`);
|
||||||
|
deps.setExitCode(1);
|
||||||
|
deps.quitApp();
|
||||||
|
},
|
||||||
|
startAppLifecycle: (args: CliArgs) => deps.startAppLifecycle(args),
|
||||||
|
});
|
||||||
|
}
|
||||||
64
src/main/runtime/startup-config-main-deps.test.ts
Normal file
64
src/main/runtime/startup-config-main-deps.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createBuildCriticalConfigErrorMainDepsHandler,
|
||||||
|
createBuildReloadConfigMainDepsHandler,
|
||||||
|
} from './startup-config-main-deps';
|
||||||
|
|
||||||
|
test('reload config main deps builder maps callbacks and fail handlers', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildReloadConfigMainDepsHandler({
|
||||||
|
reloadConfigStrict: () => ({ ok: true }),
|
||||||
|
logInfo: (message) => calls.push(`info:${message}`),
|
||||||
|
logWarning: (message) => calls.push(`warn:${message}`),
|
||||||
|
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
|
||||||
|
startConfigHotReload: () => calls.push('start-hot-reload'),
|
||||||
|
refreshAnilistClientSecretState: async (options) => {
|
||||||
|
calls.push(`refresh:${options.force}`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details) => calls.push(`error:${details}`),
|
||||||
|
showErrorBox: (title, details) => calls.push(`error-box:${title}:${details}`),
|
||||||
|
quit: () => calls.push('quit'),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.reloadConfigStrict(), { ok: true });
|
||||||
|
deps.logInfo('x');
|
||||||
|
deps.logWarning('y');
|
||||||
|
deps.showDesktopNotification('SubMiner', { body: 'warn' });
|
||||||
|
deps.startConfigHotReload();
|
||||||
|
await deps.refreshAnilistClientSecretState({ force: true });
|
||||||
|
deps.failHandlers.logError('bad');
|
||||||
|
deps.failHandlers.showErrorBox('Oops', 'Details');
|
||||||
|
deps.failHandlers.quit();
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'info:x',
|
||||||
|
'warn:y',
|
||||||
|
'notify:SubMiner:warn',
|
||||||
|
'start-hot-reload',
|
||||||
|
'refresh:true',
|
||||||
|
'error:bad',
|
||||||
|
'error-box:Oops:Details',
|
||||||
|
'quit',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('critical config main deps builder maps config path and fail handlers', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildCriticalConfigErrorMainDepsHandler({
|
||||||
|
getConfigPath: () => '/tmp/config.jsonc',
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details) => calls.push(`error:${details}`),
|
||||||
|
showErrorBox: (title, details) => calls.push(`error-box:${title}:${details}`),
|
||||||
|
quit: () => calls.push('quit'),
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.getConfigPath(), '/tmp/config.jsonc');
|
||||||
|
deps.failHandlers.logError('bad');
|
||||||
|
deps.failHandlers.showErrorBox('Oops', 'Details');
|
||||||
|
deps.failHandlers.quit();
|
||||||
|
assert.deepEqual(calls, ['error:bad', 'error-box:Oops:Details', 'quit']);
|
||||||
|
});
|
||||||
47
src/main/runtime/startup-config-main-deps.ts
Normal file
47
src/main/runtime/startup-config-main-deps.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
export function createBuildReloadConfigMainDepsHandler(deps: {
|
||||||
|
reloadConfigStrict: () => unknown;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
logWarning: (message: string) => void;
|
||||||
|
showDesktopNotification: (title: string, options: { body: string }) => void;
|
||||||
|
startConfigHotReload: () => void;
|
||||||
|
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>;
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details: string) => void;
|
||||||
|
showErrorBox: (title: string, details: string) => void;
|
||||||
|
quit: () => void;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
reloadConfigStrict: () => deps.reloadConfigStrict() as never,
|
||||||
|
logInfo: (message: string) => deps.logInfo(message),
|
||||||
|
logWarning: (message: string) => deps.logWarning(message),
|
||||||
|
showDesktopNotification: (title: string, options: { body: string }) =>
|
||||||
|
deps.showDesktopNotification(title, options),
|
||||||
|
startConfigHotReload: () => deps.startConfigHotReload(),
|
||||||
|
refreshAnilistClientSecretState: (options: { force: boolean }) =>
|
||||||
|
deps.refreshAnilistClientSecretState(options),
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details: string) => deps.failHandlers.logError(details),
|
||||||
|
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||||
|
quit: () => deps.failHandlers.quit(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildCriticalConfigErrorMainDepsHandler(deps: {
|
||||||
|
getConfigPath: () => string;
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details: string) => void;
|
||||||
|
showErrorBox: (title: string, details: string) => void;
|
||||||
|
quit: () => void;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
getConfigPath: () => deps.getConfigPath(),
|
||||||
|
failHandlers: {
|
||||||
|
logError: (details: string) => deps.failHandlers.logError(details),
|
||||||
|
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details),
|
||||||
|
quit: () => deps.failHandlers.quit(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
35
src/main/runtime/startup-lifecycle-main-deps.test.ts
Normal file
35
src/main/runtime/startup-lifecycle-main-deps.test.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildAppLifecycleRuntimeRunnerMainDepsHandler } from './startup-lifecycle-main-deps';
|
||||||
|
|
||||||
|
test('app lifecycle runtime runner main deps builder maps lifecycle callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildAppLifecycleRuntimeRunnerMainDepsHandler({
|
||||||
|
app: {} as never,
|
||||||
|
platform: 'darwin',
|
||||||
|
shouldStartApp: () => true,
|
||||||
|
parseArgs: () => ({}) as never,
|
||||||
|
handleCliCommand: () => calls.push('handle-cli'),
|
||||||
|
printHelp: () => calls.push('help'),
|
||||||
|
logNoRunningInstance: () => calls.push('no-instance'),
|
||||||
|
onReady: async () => {
|
||||||
|
calls.push('ready');
|
||||||
|
},
|
||||||
|
onWillQuitCleanup: () => calls.push('cleanup'),
|
||||||
|
shouldRestoreWindowsOnActivate: () => true,
|
||||||
|
restoreWindowsOnActivate: () => calls.push('restore'),
|
||||||
|
shouldQuitOnWindowAllClosed: () => false,
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.platform, 'darwin');
|
||||||
|
assert.equal(deps.shouldStartApp({} as never), true);
|
||||||
|
deps.handleCliCommand({} as never, 'initial');
|
||||||
|
deps.printHelp();
|
||||||
|
deps.logNoRunningInstance();
|
||||||
|
await deps.onReady();
|
||||||
|
deps.onWillQuitCleanup();
|
||||||
|
deps.restoreWindowsOnActivate();
|
||||||
|
assert.equal(deps.shouldRestoreWindowsOnActivate(), true);
|
||||||
|
assert.equal(deps.shouldQuitOnWindowAllClosed(), false);
|
||||||
|
assert.deepEqual(calls, ['handle-cli', 'help', 'no-instance', 'ready', 'cleanup', 'restore']);
|
||||||
|
});
|
||||||
20
src/main/runtime/startup-lifecycle-main-deps.ts
Normal file
20
src/main/runtime/startup-lifecycle-main-deps.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { AppLifecycleRuntimeRunnerParams } from '../startup-lifecycle';
|
||||||
|
|
||||||
|
export function createBuildAppLifecycleRuntimeRunnerMainDepsHandler(
|
||||||
|
deps: AppLifecycleRuntimeRunnerParams,
|
||||||
|
) {
|
||||||
|
return (): AppLifecycleRuntimeRunnerParams => ({
|
||||||
|
app: deps.app,
|
||||||
|
platform: deps.platform,
|
||||||
|
shouldStartApp: deps.shouldStartApp,
|
||||||
|
parseArgs: deps.parseArgs,
|
||||||
|
handleCliCommand: deps.handleCliCommand,
|
||||||
|
printHelp: deps.printHelp,
|
||||||
|
logNoRunningInstance: deps.logNoRunningInstance,
|
||||||
|
onReady: deps.onReady,
|
||||||
|
onWillQuitCleanup: deps.onWillQuitCleanup,
|
||||||
|
shouldRestoreWindowsOnActivate: deps.shouldRestoreWindowsOnActivate,
|
||||||
|
restoreWindowsOnActivate: deps.restoreWindowsOnActivate,
|
||||||
|
shouldQuitOnWindowAllClosed: deps.shouldQuitOnWindowAllClosed,
|
||||||
|
});
|
||||||
|
}
|
||||||
77
src/main/runtime/subtitle-tokenization-main-deps.test.ts
Normal file
77
src/main/runtime/subtitle-tokenization-main-deps.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createBuildTokenizerDepsMainHandler,
|
||||||
|
createCreateMecabTokenizerAndCheckMainHandler,
|
||||||
|
createPrewarmSubtitleDictionariesMainHandler,
|
||||||
|
} from './subtitle-tokenization-main-deps';
|
||||||
|
|
||||||
|
test('tokenizer deps builder records known-word lookups and maps readers', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildTokenizerDepsMainHandler({
|
||||||
|
getYomitanExt: () => ({ id: 'ext' }),
|
||||||
|
getYomitanParserWindow: () => ({ id: 'window' }),
|
||||||
|
setYomitanParserWindow: () => calls.push('set-window'),
|
||||||
|
getYomitanParserReadyPromise: () => null,
|
||||||
|
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||||
|
getYomitanParserInitPromise: () => null,
|
||||||
|
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||||
|
isKnownWord: (text) => text === 'known',
|
||||||
|
recordLookup: (hit) => calls.push(`lookup:${hit}`),
|
||||||
|
getKnownWordMatchMode: () => 'exact',
|
||||||
|
getMinSentenceWordsForNPlusOne: () => 3,
|
||||||
|
getJlptLevel: () => 'N2',
|
||||||
|
getJlptEnabled: () => true,
|
||||||
|
getFrequencyDictionaryEnabled: () => true,
|
||||||
|
getFrequencyRank: () => 5,
|
||||||
|
getYomitanGroupDebugEnabled: () => false,
|
||||||
|
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isKnownWord('known'), true);
|
||||||
|
assert.equal(deps.isKnownWord('unknown'), false);
|
||||||
|
deps.setYomitanParserWindow({});
|
||||||
|
deps.setYomitanParserReadyPromise(null);
|
||||||
|
deps.setYomitanParserInitPromise(null);
|
||||||
|
assert.equal(deps.getMinSentenceWordsForNPlusOne(), 3);
|
||||||
|
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mecab tokenizer check creates tokenizer once and runs availability check', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
type Tokenizer = { id: string };
|
||||||
|
let tokenizer: Tokenizer | null = null;
|
||||||
|
const run = createCreateMecabTokenizerAndCheckMainHandler<Tokenizer>({
|
||||||
|
getMecabTokenizer: () => tokenizer,
|
||||||
|
setMecabTokenizer: (next) => {
|
||||||
|
tokenizer = next;
|
||||||
|
calls.push('set');
|
||||||
|
},
|
||||||
|
createMecabTokenizer: () => {
|
||||||
|
calls.push('create');
|
||||||
|
return { id: 'mecab' };
|
||||||
|
},
|
||||||
|
checkAvailability: async () => {
|
||||||
|
calls.push('check');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
await run();
|
||||||
|
assert.deepEqual(calls, ['create', 'set', 'check', 'check']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dictionary prewarm runs both dictionary loaders', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const prewarm = createPrewarmSubtitleDictionariesMainHandler({
|
||||||
|
ensureJlptDictionaryLookup: async () => {
|
||||||
|
calls.push('jlpt');
|
||||||
|
},
|
||||||
|
ensureFrequencyDictionaryLookup: async () => {
|
||||||
|
calls.push('freq');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prewarm();
|
||||||
|
assert.deepEqual(calls.sort(), ['freq', 'jlpt']);
|
||||||
|
});
|
||||||
69
src/main/runtime/subtitle-tokenization-main-deps.ts
Normal file
69
src/main/runtime/subtitle-tokenization-main-deps.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export function createBuildTokenizerDepsMainHandler(deps: {
|
||||||
|
getYomitanExt: () => unknown;
|
||||||
|
getYomitanParserWindow: () => unknown;
|
||||||
|
setYomitanParserWindow: (window: unknown) => void;
|
||||||
|
getYomitanParserReadyPromise: () => Promise<void> | null;
|
||||||
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
||||||
|
getYomitanParserInitPromise: () => Promise<boolean> | null;
|
||||||
|
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
||||||
|
isKnownWord: (text: string) => boolean;
|
||||||
|
recordLookup: (hit: boolean) => void;
|
||||||
|
getKnownWordMatchMode: () => unknown;
|
||||||
|
getMinSentenceWordsForNPlusOne: () => number;
|
||||||
|
getJlptLevel: (text: string) => unknown;
|
||||||
|
getJlptEnabled: () => boolean;
|
||||||
|
getFrequencyDictionaryEnabled: () => boolean;
|
||||||
|
getFrequencyRank: (text: string) => unknown;
|
||||||
|
getYomitanGroupDebugEnabled: () => boolean;
|
||||||
|
getMecabTokenizer: () => unknown;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
getYomitanExt: () => deps.getYomitanExt() as never,
|
||||||
|
getYomitanParserWindow: () => deps.getYomitanParserWindow() as never,
|
||||||
|
setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window),
|
||||||
|
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never,
|
||||||
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
|
||||||
|
deps.setYomitanParserReadyPromise(promise),
|
||||||
|
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never,
|
||||||
|
setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
|
||||||
|
deps.setYomitanParserInitPromise(promise),
|
||||||
|
isKnownWord: (text: string) => {
|
||||||
|
const hit = deps.isKnownWord(text);
|
||||||
|
deps.recordLookup(hit);
|
||||||
|
return hit;
|
||||||
|
},
|
||||||
|
getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never,
|
||||||
|
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
|
||||||
|
getJlptLevel: (text: string) => deps.getJlptLevel(text) as never,
|
||||||
|
getJlptEnabled: () => deps.getJlptEnabled(),
|
||||||
|
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||||
|
getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never,
|
||||||
|
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
|
||||||
|
getMecabTokenizer: () => deps.getMecabTokenizer() as never,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCreateMecabTokenizerAndCheckMainHandler<TMecab>(deps: {
|
||||||
|
getMecabTokenizer: () => TMecab | null;
|
||||||
|
setMecabTokenizer: (tokenizer: TMecab) => void;
|
||||||
|
createMecabTokenizer: () => TMecab;
|
||||||
|
checkAvailability: (tokenizer: TMecab) => Promise<unknown>;
|
||||||
|
}) {
|
||||||
|
return async (): Promise<void> => {
|
||||||
|
let tokenizer = deps.getMecabTokenizer();
|
||||||
|
if (!tokenizer) {
|
||||||
|
tokenizer = deps.createMecabTokenizer();
|
||||||
|
deps.setMecabTokenizer(tokenizer);
|
||||||
|
}
|
||||||
|
await deps.checkAvailability(tokenizer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPrewarmSubtitleDictionariesMainHandler(deps: {
|
||||||
|
ensureJlptDictionaryLookup: () => Promise<void>;
|
||||||
|
ensureFrequencyDictionaryLookup: () => Promise<void>;
|
||||||
|
}) {
|
||||||
|
return async (): Promise<void> => {
|
||||||
|
await Promise.all([deps.ensureJlptDictionaryLookup(), deps.ensureFrequencyDictionaryLookup()]);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user