mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
refactor: extract runtime dependency builders from main
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-20T06:56:20Z` |
|
| `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-20T07:35:01Z` |
|
||||||
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
|
||||||
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
|
||||||
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
|
||||||
|
|||||||
@@ -9,6 +9,16 @@
|
|||||||
|
|
||||||
## Current Work (newest first)
|
## Current Work (newest first)
|
||||||
|
|
||||||
|
- [2026-02-20T07:35:01Z] progress: extracted subtitle processing controller deps assembly into `src/main/runtime/subtitle-processing-main-deps.ts` (`createBuildSubtitleProcessingControllerMainDepsHandler`) and rewired `subtitleProcessingController` construction in `src/main.ts`.
|
||||||
|
- [2026-02-20T07:35:01Z] progress: added parity tests in `src/main/runtime/subtitle-processing-main-deps.test.ts`; `src/main.ts` now 2775 LOC.
|
||||||
|
- [2026-02-20T07:35:01Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/subtitle-processing-main-deps.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/jellyfin-remote-main-deps.test.js` pass (9/9).
|
||||||
|
- [2026-02-20T07:33:56Z] progress: extracted Jellyfin remote deps assembly into `src/main/runtime/jellyfin-remote-main-deps.ts` (play/playstate/general-command/progress/stopped builders) and rewired corresponding constructor wiring in `src/main.ts` to builder-based deps.
|
||||||
|
- [2026-02-20T07:33:56Z] progress: added parity tests in `src/main/runtime/jellyfin-remote-main-deps.test.ts`; `src/main.ts` now 2770 LOC.
|
||||||
|
- [2026-02-20T07:33:56Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/jellyfin-remote-main-deps.test.js dist/main/runtime/jellyfin-remote-commands.test.js dist/main/runtime/jellyfin-remote-playback.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js` pass (18/18).
|
||||||
|
- [2026-02-20T07:31:59Z] progress: extracted config hot-reload dependency assembly into `src/main/runtime/config-hot-reload-main-deps.ts` (`createWatchConfigPathHandler`, `createBuildConfigHotReloadAppliedMainDepsHandler`, `createBuildConfigHotReloadRuntimeMainDepsHandler`) and rewired `configHotReloadRuntime` construction in `src/main.ts` to builder-based deps.
|
||||||
|
- [2026-02-20T07:31:59Z] progress: preserved runtime parity by keeping debounce/watch-path behavior and `createConfigHotReloadAppliedHandler` side-effects unchanged while moving inline glue code out of `src/main.ts`.
|
||||||
|
- [2026-02-20T07:31:59Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/config-hot-reload-main-deps.test.js dist/main/runtime/config-hot-reload-handlers.test.js dist/core/services/config-hot-reload.test.js` pass (12/12).
|
||||||
|
- [2026-02-20T07:31:59Z] next: continue TASK-85 by extracting next high-churn deps assembly from `src/main.ts` (candidate: startup/bootstrap or app-ready clusters) into focused `*-main-deps.ts` + parity tests.
|
||||||
- [2026-02-20T03:27:35Z] progress: extracted CLI context deps builder into `src/main/runtime/cli-command-context-deps.ts` (`createBuildCliCommandContextDepsHandler`) and rewired `handleCliCommand` to build deps through the helper.
|
- [2026-02-20T03:27:35Z] progress: extracted CLI context deps builder into `src/main/runtime/cli-command-context-deps.ts` (`createBuildCliCommandContextDepsHandler`) and rewired `handleCliCommand` to build deps through the helper.
|
||||||
- [2026-02-20T03:27:35Z] progress: extracted overlay runtime options builder into `src/main/runtime/overlay-runtime-options.ts` (`createBuildInitializeOverlayRuntimeOptionsHandler`) and rewired `initializeOverlayRuntime` `buildOptions`; extracted Yomitan extension load wrappers into `src/main/runtime/yomitan-extension-loader.ts` (`createLoadYomitanExtensionHandler`, `createEnsureYomitanExtensionLoadedHandler`) and rewired `loadYomitanExtension` + `ensureYomitanExtensionLoaded`.
|
- [2026-02-20T03:27:35Z] progress: extracted overlay runtime options builder into `src/main/runtime/overlay-runtime-options.ts` (`createBuildInitializeOverlayRuntimeOptionsHandler`) and rewired `initializeOverlayRuntime` `buildOptions`; extracted Yomitan extension load wrappers into `src/main/runtime/yomitan-extension-loader.ts` (`createLoadYomitanExtensionHandler`, `createEnsureYomitanExtensionLoadedHandler`) and rewired `loadYomitanExtension` + `ensureYomitanExtensionLoaded`.
|
||||||
- [2026-02-20T03:27:35Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/cli-command-context-deps.test.js dist/main/runtime/overlay-runtime-options.test.js dist/main/runtime/yomitan-extension-loader.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (116/116).
|
- [2026-02-20T03:27:35Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/cli-command-context-deps.test.js dist/main/runtime/overlay-runtime-options.test.js dist/main/runtime/yomitan-extension-loader.test.js dist/main/runtime/tray-main-actions.test.js dist/main/runtime/overlay-window-factory.test.js dist/main/runtime/ipc-bridge-actions.test.js dist/main/runtime/overlay-main-actions.test.js dist/main/runtime/overlay-visibility-actions.test.js dist/main/runtime/mining-actions.test.js dist/main/runtime/anki-actions.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js dist/main/runtime/numeric-shortcut-session-handlers.test.js dist/main/runtime/mpv-osd-log.test.js dist/main/runtime/global-shortcuts.test.js dist/main/runtime/yomitan-settings-opener.test.js dist/main/runtime/overlay-runtime-bootstrap.test.js dist/main/runtime/tray-lifecycle.test.js dist/main/runtime/tray-runtime.test.js dist/main/runtime/overlay-window-layout.test.js dist/main/runtime/startup-warmups.test.js dist/main/runtime/mpv-subtitle-render-metrics.test.js dist/main/runtime/mpv-client-runtime-service.test.js dist/main/runtime/mpv-client-event-bindings.test.js dist/main/runtime/cli-command-context.test.js dist/main/runtime/cli-command-prechecks.test.js dist/main/runtime/initial-args-handler.test.js dist/main/runtime/anilist-setup-window.test.js dist/main/runtime/jellyfin-setup-window.test.js dist/main/runtime/jellyfin-remote-session-lifecycle.test.js dist/main/runtime/jellyfin-cli-auth.test.js dist/main/runtime/jellyfin-cli-list.test.js dist/main/runtime/jellyfin-cli-play.test.js dist/main/runtime/jellyfin-cli-remote-announce.test.js` pass (116/116).
|
||||||
@@ -215,6 +225,16 @@
|
|||||||
- `src/main/runtime/field-grouping-resolver.test.ts`
|
- `src/main/runtime/field-grouping-resolver.test.ts`
|
||||||
- `src/main/runtime/mpv-jellyfin-defaults.ts`
|
- `src/main/runtime/mpv-jellyfin-defaults.ts`
|
||||||
- `src/main/runtime/mpv-jellyfin-defaults.test.ts`
|
- `src/main/runtime/mpv-jellyfin-defaults.test.ts`
|
||||||
|
- `src/main/runtime/field-grouping-overlay-main-deps.ts`
|
||||||
|
- `src/main/runtime/field-grouping-overlay-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/media-runtime-main-deps.ts`
|
||||||
|
- `src/main/runtime/media-runtime-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/overlay-visibility-runtime-main-deps.ts`
|
||||||
|
- `src/main/runtime/overlay-visibility-runtime-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/dictionary-runtime-main-deps.ts`
|
||||||
|
- `src/main/runtime/dictionary-runtime-main-deps.test.ts`
|
||||||
|
- `src/main/runtime/overlay-shortcuts-runtime-main-deps.ts`
|
||||||
|
- `src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts`
|
||||||
|
|
||||||
## Open Questions / Blockers
|
## Open Questions / Blockers
|
||||||
|
|
||||||
@@ -290,3 +310,20 @@
|
|||||||
- [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: 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] 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).
|
- [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).
|
||||||
|
- [2026-02-20T07:12:59Z] progress: extracted field-grouping overlay deps assembly into `src/main/runtime/field-grouping-overlay-main-deps.ts` and rewired `createFieldGroupingOverlayRuntime` setup in `src/main.ts` to use builder output.
|
||||||
|
- [2026-02-20T07:12:59Z] progress: resolved strict typing mismatch by parameterizing builder choice type and binding it to `KikuFieldGroupingChoice` in `main.ts`.
|
||||||
|
- [2026-02-20T07:12:59Z] progress: `src/main.ts` currently 2747 LOC after this slice.
|
||||||
|
- [2026-02-20T07:12:59Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/field-grouping-overlay-main-deps.test.js dist/core/services/field-grouping-overlay.test.js dist/main/runtime/field-grouping-resolver.test.js` pass (6/6).
|
||||||
|
- [2026-02-20T07:14:28Z] progress: extracted media runtime deps assembly into `src/main/runtime/media-runtime-main-deps.ts` and rewired `createMediaRuntimeService` setup in `src/main.ts` through the builder.
|
||||||
|
- [2026-02-20T07:14:28Z] progress: `src/main.ts` currently 2750 LOC after this slice.
|
||||||
|
- [2026-02-20T07:14:28Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/media-runtime-main-deps.test.js dist/main/media-runtime.test.js dist/main/runtime/field-grouping-overlay-main-deps.test.js` pass (2/2 from selected bundle output).
|
||||||
|
- [2026-02-20T07:16:22Z] progress: extracted overlay visibility runtime deps assembly into `src/main/runtime/overlay-visibility-runtime-main-deps.ts` and rewired `createOverlayVisibilityRuntimeService` setup in `src/main.ts` through the builder.
|
||||||
|
- [2026-02-20T07:16:22Z] progress: `src/main.ts` currently 2753 LOC after this slice.
|
||||||
|
- [2026-02-20T07:16:22Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-visibility-runtime-main-deps.test.js dist/main/overlay-visibility-runtime.test.js dist/main/runtime/media-runtime-main-deps.test.js` pass (2/2 from selected bundle output).
|
||||||
|
- [2026-02-20T07:25:54Z] progress: extracted JLPT/frequency dictionary runtime deps assembly and root-list construction into `src/main/runtime/dictionary-runtime-main-deps.ts`; rewired `createJlptDictionaryRuntimeService` + `createFrequencyDictionaryRuntimeService` setup in `src/main.ts`.
|
||||||
|
- [2026-02-20T07:25:54Z] progress: adjusted frequency roots wiring to preserve previous behavior exactly (separate frequency roots builder, no JLPT-root leakage).
|
||||||
|
- [2026-02-20T07:25:54Z] progress: `src/main.ts` currently 2747 LOC after this slice.
|
||||||
|
- [2026-02-20T07:25:54Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/dictionary-runtime-main-deps.test.js dist/main/frequency-dictionary-runtime.test.js dist/main/jlpt-runtime.test.js` pass (4/4 from selected bundle output).
|
||||||
|
- [2026-02-20T07:27:15Z] progress: extracted overlay-shortcuts runtime deps assembly into `src/main/runtime/overlay-shortcuts-runtime-main-deps.ts` and rewired `createOverlayShortcutsRuntimeService` setup in `src/main.ts` through the builder.
|
||||||
|
- [2026-02-20T07:27:15Z] progress: `src/main.ts` currently 2750 LOC after this slice.
|
||||||
|
- [2026-02-20T07:27:15Z] test: `bun run build` pass (expected macOS helper Swift cache fallback) + `node --test dist/main/runtime/overlay-shortcuts-runtime-main-deps.test.js dist/main/overlay-shortcuts-runtime.test.js dist/main/runtime/overlay-shortcuts-lifecycle.test.js` pass (5/5).
|
||||||
|
|||||||
269
src/main.ts
269
src/main.ts
@@ -106,6 +106,14 @@ import {
|
|||||||
createReportJellyfinRemoteProgressHandler,
|
createReportJellyfinRemoteProgressHandler,
|
||||||
createReportJellyfinRemoteStoppedHandler,
|
createReportJellyfinRemoteStoppedHandler,
|
||||||
} from './main/runtime/jellyfin-remote-playback';
|
} from './main/runtime/jellyfin-remote-playback';
|
||||||
|
import {
|
||||||
|
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler,
|
||||||
|
createBuildHandleJellyfinRemotePlayMainDepsHandler,
|
||||||
|
createBuildHandleJellyfinRemotePlaystateMainDepsHandler,
|
||||||
|
createBuildReportJellyfinRemoteProgressMainDepsHandler,
|
||||||
|
createBuildReportJellyfinRemoteStoppedMainDepsHandler,
|
||||||
|
} from './main/runtime/jellyfin-remote-main-deps';
|
||||||
|
import { createBuildSubtitleProcessingControllerMainDepsHandler } from './main/runtime/subtitle-processing-main-deps';
|
||||||
import {
|
import {
|
||||||
createEnsureMpvConnectedForJellyfinPlaybackHandler,
|
createEnsureMpvConnectedForJellyfinPlaybackHandler,
|
||||||
createLaunchMpvIdleForJellyfinPlaybackHandler,
|
createLaunchMpvIdleForJellyfinPlaybackHandler,
|
||||||
@@ -156,6 +164,13 @@ import {
|
|||||||
createApplyJellyfinMpvDefaultsHandler,
|
createApplyJellyfinMpvDefaultsHandler,
|
||||||
createGetDefaultSocketPathHandler,
|
createGetDefaultSocketPathHandler,
|
||||||
} from './main/runtime/mpv-jellyfin-defaults';
|
} from './main/runtime/mpv-jellyfin-defaults';
|
||||||
|
import { createBuildMediaRuntimeMainDepsHandler } from './main/runtime/media-runtime-main-deps';
|
||||||
|
import {
|
||||||
|
createBuildDictionaryRootsMainHandler,
|
||||||
|
createBuildFrequencyDictionaryRootsMainHandler,
|
||||||
|
createBuildFrequencyDictionaryRuntimeMainDepsHandler,
|
||||||
|
createBuildJlptDictionaryRuntimeMainDepsHandler,
|
||||||
|
} from './main/runtime/dictionary-runtime-main-deps';
|
||||||
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 {
|
||||||
@@ -170,6 +185,7 @@ import {
|
|||||||
createGetFieldGroupingResolverHandler,
|
createGetFieldGroupingResolverHandler,
|
||||||
createSetFieldGroupingResolverHandler,
|
createSetFieldGroupingResolverHandler,
|
||||||
} from './main/runtime/field-grouping-resolver';
|
} from './main/runtime/field-grouping-resolver';
|
||||||
|
import { createBuildFieldGroupingOverlayMainDepsHandler } from './main/runtime/field-grouping-overlay-main-deps';
|
||||||
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';
|
||||||
@@ -221,6 +237,7 @@ import {
|
|||||||
createSyncOverlayShortcutsHandler,
|
createSyncOverlayShortcutsHandler,
|
||||||
createUnregisterOverlayShortcutsHandler,
|
createUnregisterOverlayShortcutsHandler,
|
||||||
} from './main/runtime/overlay-shortcuts-lifecycle';
|
} from './main/runtime/overlay-shortcuts-lifecycle';
|
||||||
|
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/overlay-shortcuts-runtime-main-deps';
|
||||||
import {
|
import {
|
||||||
createMarkLastCardAsAudioCardHandler,
|
createMarkLastCardAsAudioCardHandler,
|
||||||
createMineSentenceCardHandler,
|
createMineSentenceCardHandler,
|
||||||
@@ -239,6 +256,7 @@ import {
|
|||||||
createToggleInvisibleOverlayHandler,
|
createToggleInvisibleOverlayHandler,
|
||||||
createToggleVisibleOverlayHandler,
|
createToggleVisibleOverlayHandler,
|
||||||
} from './main/runtime/overlay-visibility-actions';
|
} from './main/runtime/overlay-visibility-actions';
|
||||||
|
import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './main/runtime/overlay-visibility-runtime-main-deps';
|
||||||
import {
|
import {
|
||||||
createAppendClipboardVideoToQueueHandler,
|
createAppendClipboardVideoToQueueHandler,
|
||||||
createHandleOverlayModalClosedHandler,
|
createHandleOverlayModalClosedHandler,
|
||||||
@@ -306,6 +324,11 @@ import {
|
|||||||
createConfigHotReloadMessageHandler,
|
createConfigHotReloadMessageHandler,
|
||||||
resolveSubtitleStyleForRenderer,
|
resolveSubtitleStyleForRenderer,
|
||||||
} from './main/runtime/config-hot-reload-handlers';
|
} from './main/runtime/config-hot-reload-handlers';
|
||||||
|
import {
|
||||||
|
createBuildConfigHotReloadAppliedMainDepsHandler,
|
||||||
|
createBuildConfigHotReloadRuntimeMainDepsHandler,
|
||||||
|
createWatchConfigPathHandler,
|
||||||
|
} from './main/runtime/config-hot-reload-main-deps';
|
||||||
import {
|
import {
|
||||||
createBuildCriticalConfigErrorMainDepsHandler,
|
createBuildCriticalConfigErrorMainDepsHandler,
|
||||||
createBuildReloadConfigMainDepsHandler,
|
createBuildReloadConfigMainDepsHandler,
|
||||||
@@ -607,7 +630,8 @@ const subsyncRuntime = createMainSubsyncRuntime({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
let appTray: Tray | null = null;
|
let appTray: Tray | null = null;
|
||||||
const subtitleProcessingController = createSubtitleProcessingController({
|
const buildSubtitleProcessingControllerMainDepsHandler =
|
||||||
|
createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||||
tokenizeSubtitle: async (text: string) => {
|
tokenizeSubtitle: async (text: string) => {
|
||||||
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -627,7 +651,11 @@ const subtitleProcessingController = createSubtitleProcessingController({
|
|||||||
},
|
},
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
});
|
});
|
||||||
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService({
|
const subtitleProcessingController = createSubtitleProcessingController(
|
||||||
|
buildSubtitleProcessingControllerMainDepsHandler(),
|
||||||
|
);
|
||||||
|
const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
||||||
|
createBuildOverlayShortcutsRuntimeMainDepsHandler({
|
||||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||||
getShortcutsRegistered: () => appState.shortcutsRegistered,
|
getShortcutsRegistered: () => appState.shortcutsRegistered,
|
||||||
setShortcutsRegistered: (registered) => {
|
setShortcutsRegistered: (registered) => {
|
||||||
@@ -664,13 +692,55 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService({
|
|||||||
cancelPendingMineSentenceMultiple: () => {
|
cancelPendingMineSentenceMultiple: () => {
|
||||||
cancelPendingMineSentenceMultiple();
|
cancelPendingMineSentenceMultiple();
|
||||||
},
|
},
|
||||||
});
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler({
|
const notifyConfigHotReloadMessage = createConfigHotReloadMessageHandler({
|
||||||
showMpvOsd: (message) => showMpvOsd(message),
|
showMpvOsd: (message) => showMpvOsd(message),
|
||||||
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
showDesktopNotification: (title, options) => showDesktopNotification(title, options),
|
||||||
});
|
});
|
||||||
const handleJellyfinRemotePlay = createHandleJellyfinRemotePlay({
|
const watchConfigPathHandler = createWatchConfigPathHandler({
|
||||||
|
fileExists: (targetPath) => fs.existsSync(targetPath),
|
||||||
|
dirname: (targetPath) => path.dirname(targetPath),
|
||||||
|
watchPath: (targetPath, listener) => fs.watch(targetPath, listener),
|
||||||
|
});
|
||||||
|
const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler({
|
||||||
|
setKeybindings: (keybindings) => {
|
||||||
|
appState.keybindings = keybindings as never;
|
||||||
|
},
|
||||||
|
refreshGlobalAndOverlayShortcuts: () => {
|
||||||
|
refreshGlobalAndOverlayShortcuts();
|
||||||
|
},
|
||||||
|
setSecondarySubMode: (mode) => {
|
||||||
|
appState.secondarySubMode = mode as never;
|
||||||
|
},
|
||||||
|
broadcastToOverlayWindows: (channel, payload) => {
|
||||||
|
broadcastToOverlayWindows(channel, payload);
|
||||||
|
},
|
||||||
|
applyAnkiRuntimeConfigPatch: (patch) => {
|
||||||
|
if (appState.ankiIntegration) {
|
||||||
|
appState.ankiIntegration.applyRuntimeConfigPatch(patch as never);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const buildConfigHotReloadRuntimeMainDepsHandler = createBuildConfigHotReloadRuntimeMainDepsHandler({
|
||||||
|
getCurrentConfig: () => getResolvedConfig(),
|
||||||
|
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
||||||
|
watchConfigPath: (configPath, onChange) => watchConfigPathHandler(configPath, onChange),
|
||||||
|
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
||||||
|
clearTimeout: (timeout) => clearTimeout(timeout),
|
||||||
|
debounceMs: 250,
|
||||||
|
onHotReloadApplied: createConfigHotReloadAppliedHandler(buildConfigHotReloadAppliedMainDepsHandler()),
|
||||||
|
onRestartRequired: (fields) => notifyConfigHotReloadMessage(buildRestartRequiredConfigMessage(fields)),
|
||||||
|
onInvalidConfig: notifyConfigHotReloadMessage,
|
||||||
|
onValidationWarnings: (configPath, warnings) => {
|
||||||
|
showDesktopNotification('SubMiner', {
|
||||||
|
body: buildConfigWarningNotificationBody(configPath, warnings),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const buildHandleJellyfinRemotePlayMainDepsHandler =
|
||||||
|
createBuildHandleJellyfinRemotePlayMainDepsHandler({
|
||||||
getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()),
|
getConfiguredSession: () => getConfiguredJellyfinSession(getResolvedJellyfinConfig()),
|
||||||
getClientInfo: () => getJellyfinClientInfo(),
|
getClientInfo: () => getJellyfinClientInfo(),
|
||||||
getJellyfinConfig: () => getResolvedJellyfinConfig(),
|
getJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||||
@@ -678,21 +748,24 @@ const handleJellyfinRemotePlay = createHandleJellyfinRemotePlay({
|
|||||||
playJellyfinItemInMpv(params as Parameters<typeof playJellyfinItemInMpv>[0]),
|
playJellyfinItemInMpv(params as Parameters<typeof playJellyfinItemInMpv>[0]),
|
||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
});
|
});
|
||||||
const handleJellyfinRemotePlaystate = createHandleJellyfinRemotePlaystate({
|
const buildHandleJellyfinRemotePlaystateMainDepsHandler =
|
||||||
|
createBuildHandleJellyfinRemotePlaystateMainDepsHandler({
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command),
|
sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command),
|
||||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||||
reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(),
|
||||||
jellyfinTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks),
|
jellyfinTicksToSeconds: (ticks) => jellyfinTicksToSecondsRuntime(ticks),
|
||||||
});
|
});
|
||||||
const handleJellyfinRemoteGeneralCommand = createHandleJellyfinRemoteGeneralCommand({
|
const buildHandleJellyfinRemoteGeneralCommandMainDepsHandler =
|
||||||
|
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command),
|
sendMpvCommand: (client, command) => sendMpvCommandRuntime(client as MpvIpcClient, command),
|
||||||
getActivePlayback: () => activeJellyfinRemotePlayback,
|
getActivePlayback: () => activeJellyfinRemotePlayback,
|
||||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||||
logDebug: (message) => logger.debug(message),
|
logDebug: (message) => logger.debug(message),
|
||||||
});
|
});
|
||||||
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler({
|
const buildReportJellyfinRemoteProgressMainDepsHandler =
|
||||||
|
createBuildReportJellyfinRemoteProgressMainDepsHandler({
|
||||||
getActivePlayback: () => activeJellyfinRemotePlayback,
|
getActivePlayback: () => activeJellyfinRemotePlayback,
|
||||||
clearActivePlayback: () => {
|
clearActivePlayback: () => {
|
||||||
activeJellyfinRemotePlayback = null;
|
activeJellyfinRemotePlayback = null;
|
||||||
@@ -708,7 +781,8 @@ const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler({
|
|||||||
ticksPerSecond: JELLYFIN_TICKS_PER_SECOND,
|
ticksPerSecond: JELLYFIN_TICKS_PER_SECOND,
|
||||||
logDebug: (message, error) => logger.debug(message, error),
|
logDebug: (message, error) => logger.debug(message, error),
|
||||||
});
|
});
|
||||||
const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler({
|
const buildReportJellyfinRemoteStoppedMainDepsHandler =
|
||||||
|
createBuildReportJellyfinRemoteStoppedMainDepsHandler({
|
||||||
getActivePlayback: () => activeJellyfinRemotePlayback,
|
getActivePlayback: () => activeJellyfinRemotePlayback,
|
||||||
clearActivePlayback: () => {
|
clearActivePlayback: () => {
|
||||||
activeJellyfinRemotePlayback = null;
|
activeJellyfinRemotePlayback = null;
|
||||||
@@ -716,118 +790,69 @@ const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler({
|
|||||||
getSession: () => appState.jellyfinRemoteSession,
|
getSession: () => appState.jellyfinRemoteSession,
|
||||||
logDebug: (message, error) => logger.debug(message, error),
|
logDebug: (message, error) => logger.debug(message, error),
|
||||||
});
|
});
|
||||||
|
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler(
|
||||||
|
buildReportJellyfinRemoteProgressMainDepsHandler(),
|
||||||
|
);
|
||||||
|
const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler(
|
||||||
|
buildReportJellyfinRemoteStoppedMainDepsHandler(),
|
||||||
|
);
|
||||||
|
const handleJellyfinRemotePlay = createHandleJellyfinRemotePlay(
|
||||||
|
buildHandleJellyfinRemotePlayMainDepsHandler(),
|
||||||
|
);
|
||||||
|
const handleJellyfinRemotePlaystate = createHandleJellyfinRemotePlaystate(
|
||||||
|
buildHandleJellyfinRemotePlaystateMainDepsHandler(),
|
||||||
|
);
|
||||||
|
const handleJellyfinRemoteGeneralCommand = createHandleJellyfinRemoteGeneralCommand(
|
||||||
|
buildHandleJellyfinRemoteGeneralCommandMainDepsHandler(),
|
||||||
|
);
|
||||||
|
|
||||||
const configHotReloadRuntime = createConfigHotReloadRuntime({
|
const configHotReloadRuntime = createConfigHotReloadRuntime(buildConfigHotReloadRuntimeMainDepsHandler());
|
||||||
getCurrentConfig: () => getResolvedConfig(),
|
|
||||||
reloadConfigStrict: () => configService.reloadConfigStrict(),
|
|
||||||
watchConfigPath: (configPath, onChange) => {
|
|
||||||
const watchTarget = fs.existsSync(configPath) ? configPath : path.dirname(configPath);
|
|
||||||
const watcher = fs.watch(watchTarget, (_eventType, filename) => {
|
|
||||||
if (watchTarget === configPath) {
|
|
||||||
onChange();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized =
|
const buildDictionaryRootsHandler = createBuildDictionaryRootsMainHandler({
|
||||||
typeof filename === 'string' ? filename : filename ? String(filename) : undefined;
|
dirname: __dirname,
|
||||||
if (!normalized || normalized === 'config.json' || normalized === 'config.jsonc') {
|
appPath: app.getAppPath(),
|
||||||
onChange();
|
resourcesPath: process.resourcesPath,
|
||||||
}
|
userDataPath: USER_DATA_PATH,
|
||||||
|
appUserDataPath: app.getPath('userData'),
|
||||||
|
homeDir: os.homedir(),
|
||||||
|
cwd: process.cwd(),
|
||||||
|
joinPath: (...parts) => path.join(...parts),
|
||||||
});
|
});
|
||||||
return {
|
const buildFrequencyDictionaryRootsHandler = createBuildFrequencyDictionaryRootsMainHandler({
|
||||||
close: () => {
|
dirname: __dirname,
|
||||||
watcher.close();
|
appPath: app.getAppPath(),
|
||||||
},
|
resourcesPath: process.resourcesPath,
|
||||||
};
|
userDataPath: USER_DATA_PATH,
|
||||||
},
|
appUserDataPath: app.getPath('userData'),
|
||||||
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
homeDir: os.homedir(),
|
||||||
clearTimeout: (timeout) => clearTimeout(timeout),
|
cwd: process.cwd(),
|
||||||
debounceMs: 250,
|
joinPath: (...parts) => path.join(...parts),
|
||||||
onHotReloadApplied: createConfigHotReloadAppliedHandler({
|
|
||||||
setKeybindings: (keybindings) => {
|
|
||||||
appState.keybindings = keybindings;
|
|
||||||
},
|
|
||||||
refreshGlobalAndOverlayShortcuts: () => {
|
|
||||||
refreshGlobalAndOverlayShortcuts();
|
|
||||||
},
|
|
||||||
setSecondarySubMode: (mode) => {
|
|
||||||
appState.secondarySubMode = mode;
|
|
||||||
},
|
|
||||||
broadcastToOverlayWindows: (channel, payload) => {
|
|
||||||
broadcastToOverlayWindows(channel, payload);
|
|
||||||
},
|
|
||||||
applyAnkiRuntimeConfigPatch: (patch) => {
|
|
||||||
if (appState.ankiIntegration) {
|
|
||||||
appState.ankiIntegration.applyRuntimeConfigPatch(patch);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
onRestartRequired: (fields) => notifyConfigHotReloadMessage(buildRestartRequiredConfigMessage(fields)),
|
|
||||||
onInvalidConfig: notifyConfigHotReloadMessage,
|
|
||||||
onValidationWarnings: (configPath, warnings) => {
|
|
||||||
showDesktopNotification('SubMiner', {
|
|
||||||
body: buildConfigWarningNotificationBody(configPath, warnings),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const jlptDictionaryRuntime = createJlptDictionaryRuntimeService({
|
const jlptDictionaryRuntime = createJlptDictionaryRuntimeService(
|
||||||
|
createBuildJlptDictionaryRuntimeMainDepsHandler({
|
||||||
isJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
isJlptEnabled: () => getResolvedConfig().subtitleStyle.enableJlpt,
|
||||||
getSearchPaths: () =>
|
getDictionaryRoots: () => buildDictionaryRootsHandler(),
|
||||||
getJlptDictionarySearchPaths({
|
getJlptDictionarySearchPaths,
|
||||||
getDictionaryRoots: () => [
|
|
||||||
path.join(__dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
|
|
||||||
path.join(app.getAppPath(), 'vendor', 'yomitan-jlpt-vocab'),
|
|
||||||
path.join(process.resourcesPath, 'yomitan-jlpt-vocab'),
|
|
||||||
path.join(process.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
|
|
||||||
USER_DATA_PATH,
|
|
||||||
app.getPath('userData'),
|
|
||||||
path.join(os.homedir(), '.config', 'SubMiner'),
|
|
||||||
path.join(os.homedir(), '.config', 'subminer'),
|
|
||||||
path.join(os.homedir(), 'Library', 'Application Support', 'SubMiner'),
|
|
||||||
path.join(os.homedir(), 'Library', 'Application Support', 'subminer'),
|
|
||||||
process.cwd(),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
setJlptLevelLookup: (lookup) => {
|
setJlptLevelLookup: (lookup) => {
|
||||||
appState.jlptLevelLookup = lookup;
|
appState.jlptLevelLookup = lookup as never;
|
||||||
},
|
},
|
||||||
log: (message) => {
|
logInfo: (message) => logger.info(message),
|
||||||
logger.info(`[JLPT] ${message}`);
|
})(),
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService({
|
const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService(
|
||||||
|
createBuildFrequencyDictionaryRuntimeMainDepsHandler({
|
||||||
isFrequencyDictionaryEnabled: () => getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
isFrequencyDictionaryEnabled: () => getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||||
getSearchPaths: () =>
|
getDictionaryRoots: () => buildFrequencyDictionaryRootsHandler(),
|
||||||
getFrequencyDictionarySearchPaths({
|
getFrequencyDictionarySearchPaths,
|
||||||
getDictionaryRoots: () =>
|
|
||||||
[
|
|
||||||
path.join(__dirname, '..', '..', 'vendor', 'jiten_freq_global'),
|
|
||||||
path.join(__dirname, '..', '..', 'vendor', 'frequency-dictionary'),
|
|
||||||
path.join(app.getAppPath(), 'vendor', 'jiten_freq_global'),
|
|
||||||
path.join(app.getAppPath(), 'vendor', 'frequency-dictionary'),
|
|
||||||
path.join(process.resourcesPath, 'jiten_freq_global'),
|
|
||||||
path.join(process.resourcesPath, 'frequency-dictionary'),
|
|
||||||
path.join(process.resourcesPath, 'app.asar', 'vendor', 'jiten_freq_global'),
|
|
||||||
path.join(process.resourcesPath, 'app.asar', 'vendor', 'frequency-dictionary'),
|
|
||||||
USER_DATA_PATH,
|
|
||||||
app.getPath('userData'),
|
|
||||||
path.join(os.homedir(), '.config', 'SubMiner'),
|
|
||||||
path.join(os.homedir(), '.config', 'subminer'),
|
|
||||||
path.join(os.homedir(), 'Library', 'Application Support', 'SubMiner'),
|
|
||||||
path.join(os.homedir(), 'Library', 'Application Support', 'subminer'),
|
|
||||||
process.cwd(),
|
|
||||||
].filter((dictionaryRoot) => dictionaryRoot),
|
|
||||||
getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath,
|
getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath,
|
||||||
}),
|
|
||||||
setFrequencyRankLookup: (lookup) => {
|
setFrequencyRankLookup: (lookup) => {
|
||||||
appState.frequencyRankLookup = lookup;
|
appState.frequencyRankLookup = lookup as never;
|
||||||
},
|
},
|
||||||
log: (message) => {
|
logInfo: (message) => logger.info(message),
|
||||||
logger.info(`[Frequency] ${message}`);
|
})(),
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler({
|
const getFieldGroupingResolverHandler = createGetFieldGroupingResolverHandler({
|
||||||
getResolver: () => appState.fieldGroupingResolver,
|
getResolver: () => appState.fieldGroupingResolver,
|
||||||
@@ -854,7 +879,11 @@ function setFieldGroupingResolver(
|
|||||||
setFieldGroupingResolverHandler(resolver);
|
setFieldGroupingResolverHandler(resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>({
|
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>(
|
||||||
|
createBuildFieldGroupingOverlayMainDepsHandler<
|
||||||
|
OverlayHostedModal,
|
||||||
|
KikuFieldGroupingChoice
|
||||||
|
>({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||||
@@ -864,15 +893,16 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHos
|
|||||||
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||||
getRestoreVisibleOverlayOnModalClose: () =>
|
getRestoreVisibleOverlayOnModalClose: () =>
|
||||||
overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(),
|
overlayModalRuntime.getRestoreVisibleOverlayOnModalClose(),
|
||||||
sendToVisibleOverlay: (channel, payload, runtimeOptions) => {
|
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
|
||||||
return overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions);
|
overlayModalRuntime.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
},
|
})(),
|
||||||
});
|
);
|
||||||
const createFieldGroupingCallback = fieldGroupingOverlayRuntime.createFieldGroupingCallback;
|
const createFieldGroupingCallback = fieldGroupingOverlayRuntime.createFieldGroupingCallback;
|
||||||
|
|
||||||
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, 'subtitle-positions');
|
const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, 'subtitle-positions');
|
||||||
|
|
||||||
const mediaRuntime = createMediaRuntimeService({
|
const mediaRuntime = createMediaRuntimeService(
|
||||||
|
createBuildMediaRuntimeMainDepsHandler({
|
||||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
||||||
loadSubtitlePosition: () => loadSubtitlePosition(),
|
loadSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
getCurrentMediaPath: () => appState.currentMediaPath,
|
getCurrentMediaPath: () => appState.currentMediaPath,
|
||||||
@@ -887,16 +917,18 @@ const mediaRuntime = createMediaRuntimeService({
|
|||||||
setSubtitlePosition: (position: SubtitlePosition | null) => {
|
setSubtitlePosition: (position: SubtitlePosition | null) => {
|
||||||
appState.subtitlePosition = position;
|
appState.subtitlePosition = position;
|
||||||
},
|
},
|
||||||
broadcastSubtitlePosition: (position) => {
|
broadcastToOverlayWindows: (channel, payload) => {
|
||||||
broadcastToOverlayWindows('subtitle-position:set', position);
|
broadcastToOverlayWindows(channel, payload);
|
||||||
},
|
},
|
||||||
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
||||||
setCurrentMediaTitle: (title) => {
|
setCurrentMediaTitle: (title) => {
|
||||||
appState.currentMediaTitle = title;
|
appState.currentMediaTitle = title;
|
||||||
},
|
},
|
||||||
});
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService({
|
const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||||
|
createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
@@ -918,7 +950,8 @@ const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService({
|
|||||||
syncOverlayShortcuts: () => {
|
syncOverlayShortcuts: () => {
|
||||||
overlayShortcutsRuntime.syncOverlayShortcuts();
|
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||||
},
|
},
|
||||||
});
|
})(),
|
||||||
|
);
|
||||||
|
|
||||||
const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler({
|
const getRuntimeOptionsStateHandler = createGetRuntimeOptionsStateHandler({
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
|
|||||||
107
src/main/runtime/config-hot-reload-main-deps.test.ts
Normal file
107
src/main/runtime/config-hot-reload-main-deps.test.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import type { ReloadConfigStrictResult } from '../../config';
|
||||||
|
import type { ResolvedConfig } from '../../types';
|
||||||
|
import {
|
||||||
|
createBuildConfigHotReloadAppliedMainDepsHandler,
|
||||||
|
createBuildConfigHotReloadRuntimeMainDepsHandler,
|
||||||
|
createWatchConfigPathHandler,
|
||||||
|
} from './config-hot-reload-main-deps';
|
||||||
|
|
||||||
|
test('watch config path handler watches file directly when config exists', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const watchConfigPath = createWatchConfigPathHandler({
|
||||||
|
fileExists: () => true,
|
||||||
|
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||||
|
watchPath: (targetPath, nextListener) => {
|
||||||
|
calls.push(`watch:${targetPath}`);
|
||||||
|
nextListener('change', 'ignored');
|
||||||
|
return { close: () => calls.push('close') };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const watcher = watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||||
|
watcher.close();
|
||||||
|
assert.deepEqual(calls, ['watch:/tmp/config.jsonc', 'change', 'close']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watch config path handler filters directory events to config files only', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const watchConfigPath = createWatchConfigPathHandler({
|
||||||
|
fileExists: () => false,
|
||||||
|
dirname: (path) => path.split('/').slice(0, -1).join('/'),
|
||||||
|
watchPath: (_targetPath, nextListener) => {
|
||||||
|
nextListener('change', 'foo.txt');
|
||||||
|
nextListener('change', 'config.json');
|
||||||
|
nextListener('change', 'config.jsonc');
|
||||||
|
nextListener('change', null);
|
||||||
|
return { close: () => {} };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watchConfigPath('/tmp/config.jsonc', () => calls.push('change'));
|
||||||
|
assert.deepEqual(calls, ['change', 'change', 'change']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config hot reload applied main deps builder maps callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildConfigHotReloadAppliedMainDepsHandler({
|
||||||
|
setKeybindings: () => calls.push('keybindings'),
|
||||||
|
refreshGlobalAndOverlayShortcuts: () => calls.push('refresh-shortcuts'),
|
||||||
|
setSecondarySubMode: () => calls.push('set-secondary'),
|
||||||
|
broadcastToOverlayWindows: (channel) => calls.push(`broadcast:${channel}`),
|
||||||
|
applyAnkiRuntimeConfigPatch: () => calls.push('apply-anki'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
deps.setKeybindings([]);
|
||||||
|
deps.refreshGlobalAndOverlayShortcuts();
|
||||||
|
deps.setSecondarySubMode('hover');
|
||||||
|
deps.broadcastToOverlayWindows('config:hot-reload', {});
|
||||||
|
deps.applyAnkiRuntimeConfigPatch({ ai: {} as never });
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'keybindings',
|
||||||
|
'refresh-shortcuts',
|
||||||
|
'set-secondary',
|
||||||
|
'broadcast:config:hot-reload',
|
||||||
|
'apply-anki',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({
|
||||||
|
getCurrentConfig: () => ({ id: 1 } as never as ResolvedConfig),
|
||||||
|
reloadConfigStrict: () =>
|
||||||
|
({
|
||||||
|
ok: true,
|
||||||
|
config: { id: 1 } as never as ResolvedConfig,
|
||||||
|
warnings: [],
|
||||||
|
path: '/tmp/config.jsonc',
|
||||||
|
}) as ReloadConfigStrictResult,
|
||||||
|
watchConfigPath: (_configPath, _onChange) => ({ close: () => calls.push('close') }),
|
||||||
|
setTimeout: (callback) => {
|
||||||
|
callback();
|
||||||
|
return 1 as never;
|
||||||
|
},
|
||||||
|
clearTimeout: () => calls.push('clear-timeout'),
|
||||||
|
debounceMs: 250,
|
||||||
|
onHotReloadApplied: () => calls.push('hot-reload'),
|
||||||
|
onRestartRequired: () => calls.push('restart-required'),
|
||||||
|
onInvalidConfig: () => calls.push('invalid-config'),
|
||||||
|
onValidationWarnings: () => calls.push('validation-warnings'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getCurrentConfig(), { id: 1 });
|
||||||
|
assert.deepEqual(deps.reloadConfigStrict(), {
|
||||||
|
ok: true,
|
||||||
|
config: { id: 1 },
|
||||||
|
warnings: [],
|
||||||
|
path: '/tmp/config.jsonc',
|
||||||
|
});
|
||||||
|
assert.equal(deps.debounceMs, 250);
|
||||||
|
deps.onHotReloadApplied({} as never, {} as never);
|
||||||
|
deps.onRestartRequired([]);
|
||||||
|
deps.onInvalidConfig('bad');
|
||||||
|
deps.onValidationWarnings('/tmp/config.jsonc', []);
|
||||||
|
assert.deepEqual(calls, ['hot-reload', 'restart-required', 'invalid-config', 'validation-warnings']);
|
||||||
|
});
|
||||||
79
src/main/runtime/config-hot-reload-main-deps.ts
Normal file
79
src/main/runtime/config-hot-reload-main-deps.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type {
|
||||||
|
ConfigHotReloadDiff,
|
||||||
|
ConfigHotReloadRuntimeDeps,
|
||||||
|
} from '../../core/services/config-hot-reload';
|
||||||
|
import type { ReloadConfigStrictResult } from '../../config';
|
||||||
|
import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types';
|
||||||
|
|
||||||
|
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
||||||
|
|
||||||
|
export function createWatchConfigPathHandler(deps: {
|
||||||
|
fileExists: (path: string) => boolean;
|
||||||
|
dirname: (path: string) => string;
|
||||||
|
watchPath: (targetPath: string, listener: ConfigWatchListener) => { close: () => void };
|
||||||
|
}) {
|
||||||
|
return (configPath: string, onChange: () => void): { close: () => void } => {
|
||||||
|
const watchTarget = deps.fileExists(configPath) ? configPath : deps.dirname(configPath);
|
||||||
|
const watcher = deps.watchPath(watchTarget, (_eventType, filename) => {
|
||||||
|
if (watchTarget === configPath) {
|
||||||
|
onChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized =
|
||||||
|
typeof filename === 'string' ? filename : filename ? String(filename) : undefined;
|
||||||
|
if (!normalized || normalized === 'config.json' || normalized === 'config.jsonc') {
|
||||||
|
onChange();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
close: () => watcher.close(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildConfigHotReloadAppliedMainDepsHandler(deps: {
|
||||||
|
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) => void;
|
||||||
|
refreshGlobalAndOverlayShortcuts: () => void;
|
||||||
|
setSecondarySubMode: (mode: SecondarySubMode) => void;
|
||||||
|
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||||
|
applyAnkiRuntimeConfigPatch: (patch: { ai: ResolvedConfig['ankiConnect']['ai'] }) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
setKeybindings: (keybindings: ConfigHotReloadPayload['keybindings']) =>
|
||||||
|
deps.setKeybindings(keybindings),
|
||||||
|
refreshGlobalAndOverlayShortcuts: () => deps.refreshGlobalAndOverlayShortcuts(),
|
||||||
|
setSecondarySubMode: (mode: SecondarySubMode) => deps.setSecondarySubMode(mode),
|
||||||
|
broadcastToOverlayWindows: (channel: string, payload: unknown) =>
|
||||||
|
deps.broadcastToOverlayWindows(channel, payload),
|
||||||
|
applyAnkiRuntimeConfigPatch: (patch: { ai: ResolvedConfig['ankiConnect']['ai'] }) =>
|
||||||
|
deps.applyAnkiRuntimeConfigPatch(patch),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildConfigHotReloadRuntimeMainDepsHandler(deps: {
|
||||||
|
getCurrentConfig: () => ResolvedConfig;
|
||||||
|
reloadConfigStrict: () => ReloadConfigStrictResult;
|
||||||
|
watchConfigPath: ConfigHotReloadRuntimeDeps['watchConfigPath'];
|
||||||
|
setTimeout: (callback: () => void, delayMs: number) => NodeJS.Timeout;
|
||||||
|
clearTimeout: (timeout: NodeJS.Timeout) => void;
|
||||||
|
debounceMs: number;
|
||||||
|
onHotReloadApplied: (diff: ConfigHotReloadDiff, config: ResolvedConfig) => void;
|
||||||
|
onRestartRequired: (fields: string[]) => void;
|
||||||
|
onInvalidConfig: (message: string) => void;
|
||||||
|
onValidationWarnings: (configPath: string, warnings: ConfigValidationWarning[]) => void;
|
||||||
|
}) {
|
||||||
|
return (): ConfigHotReloadRuntimeDeps => ({
|
||||||
|
getCurrentConfig: () => deps.getCurrentConfig(),
|
||||||
|
reloadConfigStrict: () => deps.reloadConfigStrict(),
|
||||||
|
watchConfigPath: (configPath, onChange) => deps.watchConfigPath(configPath, onChange),
|
||||||
|
setTimeout: (callback: () => void, delayMs: number) => deps.setTimeout(callback, delayMs),
|
||||||
|
clearTimeout: (timeout: NodeJS.Timeout) => deps.clearTimeout(timeout),
|
||||||
|
debounceMs: deps.debounceMs,
|
||||||
|
onHotReloadApplied: (diff, config) => deps.onHotReloadApplied(diff, config),
|
||||||
|
onRestartRequired: (fields: string[]) => deps.onRestartRequired(fields),
|
||||||
|
onInvalidConfig: (message: string) => deps.onInvalidConfig(message),
|
||||||
|
onValidationWarnings: (configPath: string, warnings: ConfigValidationWarning[]) =>
|
||||||
|
deps.onValidationWarnings(configPath, warnings),
|
||||||
|
});
|
||||||
|
}
|
||||||
80
src/main/runtime/dictionary-runtime-main-deps.test.ts
Normal file
80
src/main/runtime/dictionary-runtime-main-deps.test.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createBuildDictionaryRootsMainHandler,
|
||||||
|
createBuildFrequencyDictionaryRootsMainHandler,
|
||||||
|
createBuildFrequencyDictionaryRuntimeMainDepsHandler,
|
||||||
|
createBuildJlptDictionaryRuntimeMainDepsHandler,
|
||||||
|
} from './dictionary-runtime-main-deps';
|
||||||
|
|
||||||
|
test('dictionary roots main handler returns expected root list', () => {
|
||||||
|
const roots = createBuildDictionaryRootsMainHandler({
|
||||||
|
dirname: '/repo/dist/main',
|
||||||
|
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||||
|
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||||
|
userDataPath: '/Users/a/.config/SubMiner',
|
||||||
|
appUserDataPath: '/Users/a/Library/Application Support/SubMiner',
|
||||||
|
homeDir: '/Users/a',
|
||||||
|
cwd: '/repo',
|
||||||
|
joinPath: (...parts) => parts.join('/'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(roots.length, 11);
|
||||||
|
assert.equal(roots[0], '/repo/dist/main/../../vendor/yomitan-jlpt-vocab');
|
||||||
|
assert.equal(roots[10], '/repo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jlpt dictionary runtime main deps builder maps search paths and log prefix', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({
|
||||||
|
isJlptEnabled: () => true,
|
||||||
|
getDictionaryRoots: () => ['/root/a'],
|
||||||
|
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => getDictionaryRoots().map((path) => `${path}/jlpt`),
|
||||||
|
setJlptLevelLookup: () => calls.push('set-lookup'),
|
||||||
|
logInfo: (message) => calls.push(`log:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isJlptEnabled(), true);
|
||||||
|
assert.deepEqual(deps.getSearchPaths(), ['/root/a/jlpt']);
|
||||||
|
deps.setJlptLevelLookup(() => null);
|
||||||
|
deps.log('loaded');
|
||||||
|
assert.deepEqual(calls, ['set-lookup', 'log:[JLPT] loaded']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('frequency dictionary roots main handler returns expected root list', () => {
|
||||||
|
const roots = createBuildFrequencyDictionaryRootsMainHandler({
|
||||||
|
dirname: '/repo/dist/main',
|
||||||
|
appPath: '/Applications/SubMiner.app/Contents/Resources/app.asar',
|
||||||
|
resourcesPath: '/Applications/SubMiner.app/Contents/Resources',
|
||||||
|
userDataPath: '/Users/a/.config/SubMiner',
|
||||||
|
appUserDataPath: '/Users/a/Library/Application Support/SubMiner',
|
||||||
|
homeDir: '/Users/a',
|
||||||
|
cwd: '/repo',
|
||||||
|
joinPath: (...parts) => parts.join('/'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(roots.length, 15);
|
||||||
|
assert.equal(roots[0], '/repo/dist/main/../../vendor/jiten_freq_global');
|
||||||
|
assert.equal(roots[14], '/repo');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('frequency dictionary runtime main deps builder maps search paths/source and log prefix', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildFrequencyDictionaryRuntimeMainDepsHandler({
|
||||||
|
isFrequencyDictionaryEnabled: () => true,
|
||||||
|
getDictionaryRoots: () => ['/root/a', ''],
|
||||||
|
getFrequencyDictionarySearchPaths: ({ getDictionaryRoots, getSourcePath }) => [
|
||||||
|
...getDictionaryRoots().map((path) => `${path}/freq`),
|
||||||
|
getSourcePath() || '',
|
||||||
|
],
|
||||||
|
getSourcePath: () => '/custom/freq.json',
|
||||||
|
setFrequencyRankLookup: () => calls.push('set-rank'),
|
||||||
|
logInfo: (message) => calls.push(`log:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isFrequencyDictionaryEnabled(), true);
|
||||||
|
assert.deepEqual(deps.getSearchPaths(), ['/root/a/freq', '/custom/freq.json']);
|
||||||
|
deps.setFrequencyRankLookup(() => null);
|
||||||
|
deps.log('loaded');
|
||||||
|
assert.deepEqual(calls, ['set-rank', 'log:[Frequency] loaded']);
|
||||||
|
});
|
||||||
95
src/main/runtime/dictionary-runtime-main-deps.ts
Normal file
95
src/main/runtime/dictionary-runtime-main-deps.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export function createBuildDictionaryRootsMainHandler(deps: {
|
||||||
|
dirname: string;
|
||||||
|
appPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
userDataPath: string;
|
||||||
|
appUserDataPath: string;
|
||||||
|
homeDir: string;
|
||||||
|
cwd: string;
|
||||||
|
joinPath: (...parts: string[]) => string;
|
||||||
|
}) {
|
||||||
|
return () =>
|
||||||
|
[
|
||||||
|
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
|
||||||
|
deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
|
||||||
|
deps.userDataPath,
|
||||||
|
deps.appUserDataPath,
|
||||||
|
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||||
|
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||||
|
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||||
|
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||||
|
deps.cwd,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
|
||||||
|
dirname: string;
|
||||||
|
appPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
userDataPath: string;
|
||||||
|
appUserDataPath: string;
|
||||||
|
homeDir: string;
|
||||||
|
cwd: string;
|
||||||
|
joinPath: (...parts: string[]) => string;
|
||||||
|
}) {
|
||||||
|
return () => [
|
||||||
|
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'jiten_freq_global'),
|
||||||
|
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'frequency-dictionary'),
|
||||||
|
deps.joinPath(deps.appPath, 'vendor', 'jiten_freq_global'),
|
||||||
|
deps.joinPath(deps.appPath, 'vendor', 'frequency-dictionary'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'jiten_freq_global'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'frequency-dictionary'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'jiten_freq_global'),
|
||||||
|
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'frequency-dictionary'),
|
||||||
|
deps.userDataPath,
|
||||||
|
deps.appUserDataPath,
|
||||||
|
deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
|
||||||
|
deps.joinPath(deps.homeDir, '.config', 'subminer'),
|
||||||
|
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
|
||||||
|
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
|
||||||
|
deps.cwd,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
|
||||||
|
isJlptEnabled: () => boolean;
|
||||||
|
getDictionaryRoots: () => string[];
|
||||||
|
getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[];
|
||||||
|
setJlptLevelLookup: (lookup: unknown) => void;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
isJlptEnabled: () => deps.isJlptEnabled(),
|
||||||
|
getSearchPaths: () =>
|
||||||
|
deps.getJlptDictionarySearchPaths({
|
||||||
|
getDictionaryRoots: () => deps.getDictionaryRoots(),
|
||||||
|
}),
|
||||||
|
setJlptLevelLookup: (lookup: unknown) => deps.setJlptLevelLookup(lookup),
|
||||||
|
log: (message: string) => deps.logInfo(`[JLPT] ${message}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: {
|
||||||
|
isFrequencyDictionaryEnabled: () => boolean;
|
||||||
|
getDictionaryRoots: () => string[];
|
||||||
|
getFrequencyDictionarySearchPaths: (deps: {
|
||||||
|
getDictionaryRoots: () => string[];
|
||||||
|
getSourcePath: () => string | undefined;
|
||||||
|
}) => string[];
|
||||||
|
getSourcePath: () => string | undefined;
|
||||||
|
setFrequencyRankLookup: (lookup: unknown) => void;
|
||||||
|
logInfo: (message: string) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(),
|
||||||
|
getSearchPaths: () =>
|
||||||
|
deps.getFrequencyDictionarySearchPaths({
|
||||||
|
getDictionaryRoots: () => deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
|
||||||
|
getSourcePath: () => deps.getSourcePath(),
|
||||||
|
}),
|
||||||
|
setFrequencyRankLookup: (lookup: unknown) => deps.setFrequencyRankLookup(lookup),
|
||||||
|
log: (message: string) => deps.logInfo(`[Frequency] ${message}`),
|
||||||
|
});
|
||||||
|
}
|
||||||
42
src/main/runtime/field-grouping-overlay-main-deps.test.ts
Normal file
42
src/main/runtime/field-grouping-overlay-main-deps.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildFieldGroupingOverlayMainDepsHandler } from './field-grouping-overlay-main-deps';
|
||||||
|
|
||||||
|
test('field grouping overlay main deps builder maps window visibility and resolver wiring', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const modalSet = new Set<'runtime-options'>();
|
||||||
|
const resolver = (choice: unknown) => calls.push(`resolver:${choice}`);
|
||||||
|
|
||||||
|
const deps = createBuildFieldGroupingOverlayMainDepsHandler({
|
||||||
|
getMainWindow: () => ({ id: 'main' }),
|
||||||
|
getVisibleOverlayVisible: () => true,
|
||||||
|
getInvisibleOverlayVisible: () => false,
|
||||||
|
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||||
|
setInvisibleOverlayVisible: (visible) => calls.push(`invisible:${visible}`),
|
||||||
|
getResolver: () => resolver,
|
||||||
|
setResolver: (nextResolver) => {
|
||||||
|
calls.push(`set-resolver:${nextResolver ? 'set' : 'null'}`);
|
||||||
|
},
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => modalSet,
|
||||||
|
sendToActiveOverlayWindow: (channel, payload) => {
|
||||||
|
calls.push(`send:${channel}:${String(payload)}`);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getMainWindow(), { id: 'main' });
|
||||||
|
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||||
|
assert.equal(deps.getInvisibleOverlayVisible(), false);
|
||||||
|
assert.equal(deps.getResolver(), resolver);
|
||||||
|
assert.equal(deps.getRestoreVisibleOverlayOnModalClose(), modalSet);
|
||||||
|
deps.setVisibleOverlayVisible(true);
|
||||||
|
deps.setInvisibleOverlayVisible(false);
|
||||||
|
deps.setResolver(null);
|
||||||
|
assert.equal(deps.sendToVisibleOverlay('kiku:open', 1), true);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'visible:true',
|
||||||
|
'invisible:false',
|
||||||
|
'set-resolver:null',
|
||||||
|
'send:kiku:open:1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
34
src/main/runtime/field-grouping-overlay-main-deps.ts
Normal file
34
src/main/runtime/field-grouping-overlay-main-deps.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export function createBuildFieldGroupingOverlayMainDepsHandler<
|
||||||
|
TModal extends string,
|
||||||
|
TChoice,
|
||||||
|
>(deps: {
|
||||||
|
getMainWindow: () => unknown | null;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||||
|
getResolver: () => ((choice: TChoice) => void) | null;
|
||||||
|
setResolver: (resolver: ((choice: TChoice) => void) | null) => void;
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => Set<TModal>;
|
||||||
|
sendToActiveOverlayWindow: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: TModal },
|
||||||
|
) => boolean;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
getMainWindow: () => deps.getMainWindow() as never,
|
||||||
|
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||||
|
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||||
|
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||||
|
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||||
|
getResolver: () => deps.getResolver() as never,
|
||||||
|
setResolver: (resolver: ((choice: TChoice) => void) | null) => deps.setResolver(resolver),
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),
|
||||||
|
sendToVisibleOverlay: (
|
||||||
|
channel: string,
|
||||||
|
payload?: unknown,
|
||||||
|
runtimeOptions?: { restoreOnModalClose?: TModal },
|
||||||
|
) => deps.sendToActiveOverlayWindow(channel, payload, runtimeOptions),
|
||||||
|
});
|
||||||
|
}
|
||||||
114
src/main/runtime/jellyfin-remote-main-deps.test.ts
Normal file
114
src/main/runtime/jellyfin-remote-main-deps.test.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler,
|
||||||
|
createBuildHandleJellyfinRemotePlayMainDepsHandler,
|
||||||
|
createBuildHandleJellyfinRemotePlaystateMainDepsHandler,
|
||||||
|
createBuildReportJellyfinRemoteProgressMainDepsHandler,
|
||||||
|
createBuildReportJellyfinRemoteStoppedMainDepsHandler,
|
||||||
|
} from './jellyfin-remote-main-deps';
|
||||||
|
|
||||||
|
test('jellyfin remote play main deps builder maps callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildHandleJellyfinRemotePlayMainDepsHandler({
|
||||||
|
getConfiguredSession: () => ({ id: 1 }) as never,
|
||||||
|
getClientInfo: () => ({ id: 2 }) as never,
|
||||||
|
getJellyfinConfig: () => ({ id: 3 }),
|
||||||
|
playJellyfinItem: async () => {
|
||||||
|
calls.push('play');
|
||||||
|
},
|
||||||
|
logWarn: (message) => calls.push(`warn:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getConfiguredSession(), { id: 1 });
|
||||||
|
assert.deepEqual(deps.getClientInfo(), { id: 2 });
|
||||||
|
assert.deepEqual(deps.getJellyfinConfig(), { id: 3 });
|
||||||
|
await deps.playJellyfinItem({} as never);
|
||||||
|
deps.logWarn('missing');
|
||||||
|
assert.deepEqual(calls, ['play', 'warn:missing']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin remote playstate main deps builder maps callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildHandleJellyfinRemotePlaystateMainDepsHandler({
|
||||||
|
getMpvClient: () => ({ id: 1 }),
|
||||||
|
sendMpvCommand: () => calls.push('send'),
|
||||||
|
reportJellyfinRemoteProgress: async () => {
|
||||||
|
calls.push('progress');
|
||||||
|
},
|
||||||
|
reportJellyfinRemoteStopped: async () => {
|
||||||
|
calls.push('stopped');
|
||||||
|
},
|
||||||
|
jellyfinTicksToSeconds: (ticks) => ticks / 10,
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getMpvClient(), { id: 1 });
|
||||||
|
deps.sendMpvCommand({} as never, ['stop']);
|
||||||
|
await deps.reportJellyfinRemoteProgress(true);
|
||||||
|
await deps.reportJellyfinRemoteStopped();
|
||||||
|
assert.equal(deps.jellyfinTicksToSeconds(100), 10);
|
||||||
|
assert.deepEqual(calls, ['send', 'progress', 'stopped']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin remote general command main deps builder maps callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const playback = { itemId: 'abc', playMethod: 'DirectPlay' as const };
|
||||||
|
const deps = createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({
|
||||||
|
getMpvClient: () => ({ id: 1 }),
|
||||||
|
sendMpvCommand: () => calls.push('send'),
|
||||||
|
getActivePlayback: () => playback,
|
||||||
|
reportJellyfinRemoteProgress: async () => {
|
||||||
|
calls.push('progress');
|
||||||
|
},
|
||||||
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getMpvClient(), { id: 1 });
|
||||||
|
deps.sendMpvCommand({} as never, ['set_property', 'sid', 1]);
|
||||||
|
assert.deepEqual(deps.getActivePlayback(), playback);
|
||||||
|
await deps.reportJellyfinRemoteProgress(true);
|
||||||
|
deps.logDebug('ignore');
|
||||||
|
assert.deepEqual(calls, ['send', 'progress', 'debug:ignore']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin remote progress main deps builder maps callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildReportJellyfinRemoteProgressMainDepsHandler({
|
||||||
|
getActivePlayback: () => ({ itemId: 'abc', playMethod: 'DirectPlay' }),
|
||||||
|
clearActivePlayback: () => calls.push('clear'),
|
||||||
|
getSession: () => ({ id: 1, isConnected: () => true }) as never,
|
||||||
|
getMpvClient: () => ({ id: 2, requestProperty: async () => 0 }) as never,
|
||||||
|
getNow: () => 123,
|
||||||
|
getLastProgressAtMs: () => 10,
|
||||||
|
setLastProgressAtMs: () => calls.push('set-last'),
|
||||||
|
progressIntervalMs: 2500,
|
||||||
|
ticksPerSecond: 10000000,
|
||||||
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.getNow(), 123);
|
||||||
|
assert.equal(deps.getLastProgressAtMs(), 10);
|
||||||
|
deps.setLastProgressAtMs(5);
|
||||||
|
assert.equal(deps.progressIntervalMs, 2500);
|
||||||
|
assert.equal(deps.ticksPerSecond, 10000000);
|
||||||
|
deps.clearActivePlayback();
|
||||||
|
deps.logDebug('x', null);
|
||||||
|
assert.deepEqual(calls, ['set-last', 'clear', 'debug:x']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('jellyfin remote stopped main deps builder maps callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const session = { id: 1, isConnected: () => true };
|
||||||
|
const deps = createBuildReportJellyfinRemoteStoppedMainDepsHandler({
|
||||||
|
getActivePlayback: () => ({ itemId: 'abc', playMethod: 'DirectPlay' }),
|
||||||
|
clearActivePlayback: () => calls.push('clear'),
|
||||||
|
getSession: () => session as never,
|
||||||
|
logDebug: (message) => calls.push(`debug:${message}`),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.deepEqual(deps.getActivePlayback(), { itemId: 'abc', playMethod: 'DirectPlay' });
|
||||||
|
deps.clearActivePlayback();
|
||||||
|
assert.equal(deps.getSession(), session);
|
||||||
|
deps.logDebug('stopped', null);
|
||||||
|
assert.deepEqual(calls, ['clear', 'debug:stopped']);
|
||||||
|
});
|
||||||
73
src/main/runtime/jellyfin-remote-main-deps.ts
Normal file
73
src/main/runtime/jellyfin-remote-main-deps.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import type {
|
||||||
|
JellyfinRemoteGeneralCommandHandlerDeps,
|
||||||
|
JellyfinRemotePlayHandlerDeps,
|
||||||
|
JellyfinRemotePlaystateHandlerDeps,
|
||||||
|
} from './jellyfin-remote-commands';
|
||||||
|
import type {
|
||||||
|
JellyfinRemoteProgressReporterDeps,
|
||||||
|
JellyfinRemoteStoppedReporterDeps,
|
||||||
|
} from './jellyfin-remote-playback';
|
||||||
|
|
||||||
|
export function createBuildHandleJellyfinRemotePlayMainDepsHandler(
|
||||||
|
deps: JellyfinRemotePlayHandlerDeps,
|
||||||
|
) {
|
||||||
|
return (): JellyfinRemotePlayHandlerDeps => ({
|
||||||
|
getConfiguredSession: () => deps.getConfiguredSession(),
|
||||||
|
getClientInfo: () => deps.getClientInfo(),
|
||||||
|
getJellyfinConfig: () => deps.getJellyfinConfig(),
|
||||||
|
playJellyfinItem: (params) => deps.playJellyfinItem(params),
|
||||||
|
logWarn: (message: string) => deps.logWarn(message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildHandleJellyfinRemotePlaystateMainDepsHandler(
|
||||||
|
deps: JellyfinRemotePlaystateHandlerDeps,
|
||||||
|
) {
|
||||||
|
return (): JellyfinRemotePlaystateHandlerDeps => ({
|
||||||
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
sendMpvCommand: (client, command) => deps.sendMpvCommand(client, command),
|
||||||
|
reportJellyfinRemoteProgress: (force: boolean) => deps.reportJellyfinRemoteProgress(force),
|
||||||
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
|
jellyfinTicksToSeconds: (ticks: number) => deps.jellyfinTicksToSeconds(ticks),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler(
|
||||||
|
deps: JellyfinRemoteGeneralCommandHandlerDeps,
|
||||||
|
) {
|
||||||
|
return (): JellyfinRemoteGeneralCommandHandlerDeps => ({
|
||||||
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
sendMpvCommand: (client, command) => deps.sendMpvCommand(client, command),
|
||||||
|
getActivePlayback: () => deps.getActivePlayback(),
|
||||||
|
reportJellyfinRemoteProgress: (force: boolean) => deps.reportJellyfinRemoteProgress(force),
|
||||||
|
logDebug: (message: string) => deps.logDebug(message),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildReportJellyfinRemoteProgressMainDepsHandler(
|
||||||
|
deps: JellyfinRemoteProgressReporterDeps,
|
||||||
|
) {
|
||||||
|
return (): JellyfinRemoteProgressReporterDeps => ({
|
||||||
|
getActivePlayback: () => deps.getActivePlayback(),
|
||||||
|
clearActivePlayback: () => deps.clearActivePlayback(),
|
||||||
|
getSession: () => deps.getSession(),
|
||||||
|
getMpvClient: () => deps.getMpvClient(),
|
||||||
|
getNow: () => deps.getNow(),
|
||||||
|
getLastProgressAtMs: () => deps.getLastProgressAtMs(),
|
||||||
|
setLastProgressAtMs: (value: number) => deps.setLastProgressAtMs(value),
|
||||||
|
progressIntervalMs: deps.progressIntervalMs,
|
||||||
|
ticksPerSecond: deps.ticksPerSecond,
|
||||||
|
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildReportJellyfinRemoteStoppedMainDepsHandler(
|
||||||
|
deps: JellyfinRemoteStoppedReporterDeps,
|
||||||
|
) {
|
||||||
|
return (): JellyfinRemoteStoppedReporterDeps => ({
|
||||||
|
getActivePlayback: () => deps.getActivePlayback(),
|
||||||
|
clearActivePlayback: () => deps.clearActivePlayback(),
|
||||||
|
getSession: () => deps.getSession(),
|
||||||
|
logDebug: (message: string, error: unknown) => deps.logDebug(message, error),
|
||||||
|
});
|
||||||
|
}
|
||||||
54
src/main/runtime/media-runtime-main-deps.test.ts
Normal file
54
src/main/runtime/media-runtime-main-deps.test.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildMediaRuntimeMainDepsHandler } from './media-runtime-main-deps';
|
||||||
|
|
||||||
|
test('media runtime main deps builder maps state and subtitle broadcast channel', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let currentPath: string | null = '/tmp/a.mkv';
|
||||||
|
let subtitlePosition: unknown = null;
|
||||||
|
let currentTitle: string | null = 'Title';
|
||||||
|
|
||||||
|
const deps = createBuildMediaRuntimeMainDepsHandler({
|
||||||
|
isRemoteMediaPath: (mediaPath) => mediaPath.startsWith('http'),
|
||||||
|
loadSubtitlePosition: () => ({ x: 1 }) as never,
|
||||||
|
getCurrentMediaPath: () => currentPath,
|
||||||
|
getPendingSubtitlePosition: () => null,
|
||||||
|
getSubtitlePositionsDir: () => '/tmp/subs',
|
||||||
|
setCurrentMediaPath: (mediaPath) => {
|
||||||
|
currentPath = mediaPath;
|
||||||
|
calls.push(`path:${String(mediaPath)}`);
|
||||||
|
},
|
||||||
|
clearPendingSubtitlePosition: () => calls.push('clear-pending'),
|
||||||
|
setSubtitlePosition: (position) => {
|
||||||
|
subtitlePosition = position;
|
||||||
|
calls.push('set-position');
|
||||||
|
},
|
||||||
|
broadcastToOverlayWindows: (channel, payload) =>
|
||||||
|
calls.push(`broadcast:${channel}:${JSON.stringify(payload)}`),
|
||||||
|
getCurrentMediaTitle: () => currentTitle,
|
||||||
|
setCurrentMediaTitle: (title) => {
|
||||||
|
currentTitle = title;
|
||||||
|
calls.push(`title:${String(title)}`);
|
||||||
|
},
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isRemoteMediaPath('http://x'), true);
|
||||||
|
assert.equal(deps.getCurrentMediaPath(), '/tmp/a.mkv');
|
||||||
|
assert.equal(deps.getSubtitlePositionsDir(), '/tmp/subs');
|
||||||
|
assert.deepEqual(deps.loadSubtitlePosition(), { x: 1 });
|
||||||
|
deps.setCurrentMediaPath('/tmp/b.mkv');
|
||||||
|
deps.clearPendingSubtitlePosition();
|
||||||
|
deps.setSubtitlePosition({ line: 1 } as never);
|
||||||
|
deps.broadcastSubtitlePosition({ line: 1 } as never);
|
||||||
|
deps.setCurrentMediaTitle('Next');
|
||||||
|
assert.equal(currentPath, '/tmp/b.mkv');
|
||||||
|
assert.deepEqual(subtitlePosition, { line: 1 });
|
||||||
|
assert.equal(currentTitle, 'Next');
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'path:/tmp/b.mkv',
|
||||||
|
'clear-pending',
|
||||||
|
'set-position',
|
||||||
|
'broadcast:subtitle-position:set:{"line":1}',
|
||||||
|
'title:Next',
|
||||||
|
]);
|
||||||
|
});
|
||||||
30
src/main/runtime/media-runtime-main-deps.ts
Normal file
30
src/main/runtime/media-runtime-main-deps.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { SubtitlePosition } from '../../types';
|
||||||
|
|
||||||
|
export function createBuildMediaRuntimeMainDepsHandler(deps: {
|
||||||
|
isRemoteMediaPath: (mediaPath: string) => boolean;
|
||||||
|
loadSubtitlePosition: () => SubtitlePosition | null;
|
||||||
|
getCurrentMediaPath: () => string | null;
|
||||||
|
getPendingSubtitlePosition: () => SubtitlePosition | null;
|
||||||
|
getSubtitlePositionsDir: () => string;
|
||||||
|
setCurrentMediaPath: (mediaPath: string | null) => void;
|
||||||
|
clearPendingSubtitlePosition: () => void;
|
||||||
|
setSubtitlePosition: (position: SubtitlePosition | null) => void;
|
||||||
|
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||||
|
getCurrentMediaTitle: () => string | null;
|
||||||
|
setCurrentMediaTitle: (title: string | null) => void;
|
||||||
|
}) {
|
||||||
|
return () => ({
|
||||||
|
isRemoteMediaPath: (mediaPath: string) => deps.isRemoteMediaPath(mediaPath),
|
||||||
|
loadSubtitlePosition: () => deps.loadSubtitlePosition(),
|
||||||
|
getCurrentMediaPath: () => deps.getCurrentMediaPath(),
|
||||||
|
getPendingSubtitlePosition: () => deps.getPendingSubtitlePosition(),
|
||||||
|
getSubtitlePositionsDir: () => deps.getSubtitlePositionsDir(),
|
||||||
|
setCurrentMediaPath: (nextPath: string | null) => deps.setCurrentMediaPath(nextPath),
|
||||||
|
clearPendingSubtitlePosition: () => deps.clearPendingSubtitlePosition(),
|
||||||
|
setSubtitlePosition: (position: SubtitlePosition | null) => deps.setSubtitlePosition(position),
|
||||||
|
broadcastSubtitlePosition: (position: SubtitlePosition | null) =>
|
||||||
|
deps.broadcastToOverlayWindows('subtitle-position:set', position),
|
||||||
|
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
|
||||||
|
setCurrentMediaTitle: (title: string | null) => deps.setCurrentMediaTitle(title),
|
||||||
|
});
|
||||||
|
}
|
||||||
77
src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts
Normal file
77
src/main/runtime/overlay-shortcuts-runtime-main-deps.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './overlay-shortcuts-runtime-main-deps';
|
||||||
|
|
||||||
|
test('overlay shortcuts runtime main deps builder maps lifecycle and action callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let shortcutsRegistered = false;
|
||||||
|
const deps = createBuildOverlayShortcutsRuntimeMainDepsHandler({
|
||||||
|
getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never),
|
||||||
|
getShortcutsRegistered: () => shortcutsRegistered,
|
||||||
|
setShortcutsRegistered: (registered) => {
|
||||||
|
shortcutsRegistered = registered;
|
||||||
|
calls.push(`registered:${registered}`);
|
||||||
|
},
|
||||||
|
isOverlayRuntimeInitialized: () => true,
|
||||||
|
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||||
|
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||||
|
openJimaku: () => calls.push('jimaku'),
|
||||||
|
markAudioCard: async () => {
|
||||||
|
calls.push('mark-audio');
|
||||||
|
},
|
||||||
|
copySubtitleMultiple: (timeoutMs) => calls.push(`copy-multi:${timeoutMs}`),
|
||||||
|
copySubtitle: () => calls.push('copy'),
|
||||||
|
toggleSecondarySubMode: () => calls.push('toggle-sub'),
|
||||||
|
updateLastCardFromClipboard: async () => {
|
||||||
|
calls.push('update-last-card');
|
||||||
|
},
|
||||||
|
triggerFieldGrouping: async () => {
|
||||||
|
calls.push('field-grouping');
|
||||||
|
},
|
||||||
|
triggerSubsyncFromConfig: async () => {
|
||||||
|
calls.push('subsync');
|
||||||
|
},
|
||||||
|
mineSentenceCard: async () => {
|
||||||
|
calls.push('mine');
|
||||||
|
},
|
||||||
|
mineSentenceMultiple: (timeoutMs) => calls.push(`mine-multi:${timeoutMs}`),
|
||||||
|
cancelPendingMultiCopy: () => calls.push('cancel-copy'),
|
||||||
|
cancelPendingMineSentenceMultiple: () => calls.push('cancel-mine'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.isOverlayRuntimeInitialized(), true);
|
||||||
|
assert.equal(deps.getShortcutsRegistered(), false);
|
||||||
|
deps.setShortcutsRegistered(true);
|
||||||
|
assert.equal(shortcutsRegistered, true);
|
||||||
|
deps.showMpvOsd('x');
|
||||||
|
deps.openRuntimeOptionsPalette();
|
||||||
|
deps.openJimaku();
|
||||||
|
await deps.markAudioCard();
|
||||||
|
deps.copySubtitleMultiple(5000);
|
||||||
|
deps.copySubtitle();
|
||||||
|
deps.toggleSecondarySubMode();
|
||||||
|
await deps.updateLastCardFromClipboard();
|
||||||
|
await deps.triggerFieldGrouping();
|
||||||
|
await deps.triggerSubsyncFromConfig();
|
||||||
|
await deps.mineSentenceCard();
|
||||||
|
deps.mineSentenceMultiple(3000);
|
||||||
|
deps.cancelPendingMultiCopy();
|
||||||
|
deps.cancelPendingMineSentenceMultiple();
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'registered:true',
|
||||||
|
'osd:x',
|
||||||
|
'runtime-options',
|
||||||
|
'jimaku',
|
||||||
|
'mark-audio',
|
||||||
|
'copy-multi:5000',
|
||||||
|
'copy',
|
||||||
|
'toggle-sub',
|
||||||
|
'update-last-card',
|
||||||
|
'field-grouping',
|
||||||
|
'subsync',
|
||||||
|
'mine',
|
||||||
|
'mine-multi:3000',
|
||||||
|
'cancel-copy',
|
||||||
|
'cancel-mine',
|
||||||
|
]);
|
||||||
|
});
|
||||||
26
src/main/runtime/overlay-shortcuts-runtime-main-deps.ts
Normal file
26
src/main/runtime/overlay-shortcuts-runtime-main-deps.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { OverlayShortcutRuntimeServiceInput } from '../overlay-shortcuts-runtime';
|
||||||
|
|
||||||
|
export function createBuildOverlayShortcutsRuntimeMainDepsHandler(
|
||||||
|
deps: OverlayShortcutRuntimeServiceInput,
|
||||||
|
) {
|
||||||
|
return (): OverlayShortcutRuntimeServiceInput => ({
|
||||||
|
getConfiguredShortcuts: () => deps.getConfiguredShortcuts(),
|
||||||
|
getShortcutsRegistered: () => deps.getShortcutsRegistered(),
|
||||||
|
setShortcutsRegistered: (registered: boolean) => deps.setShortcutsRegistered(registered),
|
||||||
|
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||||
|
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||||
|
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||||
|
openJimaku: () => deps.openJimaku(),
|
||||||
|
markAudioCard: () => deps.markAudioCard(),
|
||||||
|
copySubtitleMultiple: (timeoutMs: number) => deps.copySubtitleMultiple(timeoutMs),
|
||||||
|
copySubtitle: () => deps.copySubtitle(),
|
||||||
|
toggleSecondarySubMode: () => deps.toggleSecondarySubMode(),
|
||||||
|
updateLastCardFromClipboard: () => deps.updateLastCardFromClipboard(),
|
||||||
|
triggerFieldGrouping: () => deps.triggerFieldGrouping(),
|
||||||
|
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||||
|
mineSentenceCard: () => deps.mineSentenceCard(),
|
||||||
|
mineSentenceMultiple: (timeoutMs: number) => deps.mineSentenceMultiple(timeoutMs),
|
||||||
|
cancelPendingMultiCopy: () => deps.cancelPendingMultiCopy(),
|
||||||
|
cancelPendingMineSentenceMultiple: () => deps.cancelPendingMineSentenceMultiple(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './overlay-visibility-runtime-main-deps';
|
||||||
|
|
||||||
|
test('overlay visibility runtime main deps builder maps state and geometry callbacks', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let trackerNotReadyWarningShown = false;
|
||||||
|
const mainWindow = { id: 'main' } as never;
|
||||||
|
const invisibleWindow = { id: 'invisible' } as never;
|
||||||
|
|
||||||
|
const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
||||||
|
getMainWindow: () => mainWindow,
|
||||||
|
getInvisibleWindow: () => invisibleWindow,
|
||||||
|
getVisibleOverlayVisible: () => true,
|
||||||
|
getInvisibleOverlayVisible: () => false,
|
||||||
|
getWindowTracker: () => ({ id: 'tracker' }),
|
||||||
|
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||||
|
setTrackerNotReadyWarningShown: (shown) => {
|
||||||
|
trackerNotReadyWarningShown = shown;
|
||||||
|
calls.push(`tracker-warning:${shown}`);
|
||||||
|
},
|
||||||
|
updateVisibleOverlayBounds: () => calls.push('visible-bounds'),
|
||||||
|
updateInvisibleOverlayBounds: () => calls.push('invisible-bounds'),
|
||||||
|
ensureOverlayWindowLevel: () => calls.push('ensure-level'),
|
||||||
|
enforceOverlayLayerOrder: () => calls.push('enforce-order'),
|
||||||
|
syncOverlayShortcuts: () => calls.push('sync-shortcuts'),
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.getMainWindow(), mainWindow);
|
||||||
|
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
|
||||||
|
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||||
|
assert.equal(deps.getInvisibleOverlayVisible(), false);
|
||||||
|
assert.equal(deps.getTrackerNotReadyWarningShown(), false);
|
||||||
|
deps.setTrackerNotReadyWarningShown(true);
|
||||||
|
deps.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||||
|
deps.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||||
|
deps.ensureOverlayWindowLevel(mainWindow);
|
||||||
|
deps.enforceOverlayLayerOrder();
|
||||||
|
deps.syncOverlayShortcuts();
|
||||||
|
assert.equal(trackerNotReadyWarningShown, true);
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'tracker-warning:true',
|
||||||
|
'visible-bounds',
|
||||||
|
'invisible-bounds',
|
||||||
|
'ensure-level',
|
||||||
|
'enforce-order',
|
||||||
|
'sync-shortcuts',
|
||||||
|
]);
|
||||||
|
});
|
||||||
34
src/main/runtime/overlay-visibility-runtime-main-deps.ts
Normal file
34
src/main/runtime/overlay-visibility-runtime-main-deps.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { BrowserWindow } from 'electron';
|
||||||
|
import type { WindowGeometry } from '../../types';
|
||||||
|
import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime';
|
||||||
|
|
||||||
|
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(deps: {
|
||||||
|
getMainWindow: () => BrowserWindow | null;
|
||||||
|
getInvisibleWindow: () => BrowserWindow | null;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
getInvisibleOverlayVisible: () => boolean;
|
||||||
|
getWindowTracker: () => unknown | null;
|
||||||
|
getTrackerNotReadyWarningShown: () => boolean;
|
||||||
|
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||||
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
|
enforceOverlayLayerOrder: () => void;
|
||||||
|
syncOverlayShortcuts: () => void;
|
||||||
|
}) {
|
||||||
|
return (): OverlayVisibilityRuntimeDeps => ({
|
||||||
|
getMainWindow: () => deps.getMainWindow(),
|
||||||
|
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||||
|
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||||
|
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||||
|
getWindowTracker: () => deps.getWindowTracker() as never,
|
||||||
|
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
|
||||||
|
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
|
||||||
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry),
|
||||||
|
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||||
|
deps.updateInvisibleOverlayBounds(geometry),
|
||||||
|
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||||
|
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
|
||||||
|
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
|
||||||
|
});
|
||||||
|
}
|
||||||
33
src/main/runtime/subtitle-processing-main-deps.test.ts
Normal file
33
src/main/runtime/subtitle-processing-main-deps.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createBuildSubtitleProcessingControllerMainDepsHandler } from './subtitle-processing-main-deps';
|
||||||
|
|
||||||
|
test('subtitle processing main deps builder maps callbacks', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const deps = createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||||
|
tokenizeSubtitle: async (text) => {
|
||||||
|
calls.push(`tokenize:${text}`);
|
||||||
|
return { text, tokens: null };
|
||||||
|
},
|
||||||
|
emitSubtitle: (payload) => calls.push(`emit:${payload.text}`),
|
||||||
|
logDebug: (message) => calls.push(`log:${message}`),
|
||||||
|
now: () => 42,
|
||||||
|
})();
|
||||||
|
|
||||||
|
const tokenized = await deps.tokenizeSubtitle('line');
|
||||||
|
deps.emitSubtitle({ text: 'line', tokens: null });
|
||||||
|
deps.logDebug?.('ok');
|
||||||
|
assert.equal(deps.now?.(), 42);
|
||||||
|
assert.deepEqual(tokenized, { text: 'line', tokens: null });
|
||||||
|
assert.deepEqual(calls, ['tokenize:line', 'emit:line', 'log:ok']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitle processing main deps builder preserves optional callbacks when absent', () => {
|
||||||
|
const deps = createBuildSubtitleProcessingControllerMainDepsHandler({
|
||||||
|
tokenizeSubtitle: async () => null,
|
||||||
|
emitSubtitle: () => {},
|
||||||
|
})();
|
||||||
|
|
||||||
|
assert.equal(deps.logDebug, undefined);
|
||||||
|
assert.equal(deps.now, undefined);
|
||||||
|
});
|
||||||
12
src/main/runtime/subtitle-processing-main-deps.ts
Normal file
12
src/main/runtime/subtitle-processing-main-deps.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { SubtitleProcessingControllerDeps } from '../../core/services/subtitle-processing-controller';
|
||||||
|
|
||||||
|
export function createBuildSubtitleProcessingControllerMainDepsHandler(
|
||||||
|
deps: SubtitleProcessingControllerDeps,
|
||||||
|
) {
|
||||||
|
return (): SubtitleProcessingControllerDeps => ({
|
||||||
|
tokenizeSubtitle: (text: string) => deps.tokenizeSubtitle(text),
|
||||||
|
emitSubtitle: (payload) => deps.emitSubtitle(payload),
|
||||||
|
logDebug: deps.logDebug,
|
||||||
|
now: deps.now,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user