refactor runtime deps wiring and docs/config updates

This commit is contained in:
2026-02-10 02:44:35 -08:00
parent 1c69452356
commit 579661fbef
35 changed files with 372 additions and 1042 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: help deps build install build-linux build-macos build-macos-unsigned install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-pnpm generate-config generate-example-config docs-dev docs-build docs-preview .PHONY: help deps build install build-linux build-macos build-macos-unsigned install-linux install-macos install-plugin uninstall uninstall-linux uninstall-macos print-dirs pretty ensure-pnpm generate-config generate-example-config docs-dev docs docs-preview
APP_NAME := subminer APP_NAME := subminer
THEME_FILE := subminer.rasi THEME_FILE := subminer.rasi
@@ -49,7 +49,7 @@ help:
" build-macos Build macOS DMG/ZIP (signed if configured)" \ " build-macos Build macOS DMG/ZIP (signed if configured)" \
" build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \ " build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \
" docs-dev Run VitePress docs dev server" \ " docs-dev Run VitePress docs dev server" \
" docs-build Build VitePress static docs" \ " docs Build VitePress static docs" \
" docs-preview Preview built VitePress docs" \ " docs-preview Preview built VitePress docs" \
" install-linux Install Linux wrapper/theme/app artifacts" \ " install-linux Install Linux wrapper/theme/app artifacts" \
" install-macos Install macOS wrapper/theme/app artifacts" \ " install-macos Install macOS wrapper/theme/app artifacts" \
@@ -137,7 +137,7 @@ generate-example-config: ensure-pnpm
docs-dev: ensure-pnpm docs-dev: ensure-pnpm
@pnpm run docs:dev @pnpm run docs:dev
docs-build: ensure-pnpm docs: ensure-pnpm
@pnpm run docs:build @pnpm run docs:build
docs-preview: ensure-pnpm docs-preview: ensure-pnpm

View File

@@ -53,7 +53,7 @@ For macOS app bundle / signing / permissions details, use `docs/installation.md`
## Quick Start ## Quick Start
1. Copy and customize [`config.example.jsonc`](config.example.jsonc) to `~/.config/SubMiner/config.jsonc`. 1. Copy and customize [`config.example.jsonc`](config.example.jsonc) to `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` if `XDG_CONFIG_HOME` is unset).
2. Start mpv with IPC enabled: 2. Start mpv with IPC enabled:
```bash ```bash
@@ -100,6 +100,7 @@ Detailed guides live in [`docs/`](docs/README.md):
- [Usage](docs/usage.md) - [Usage](docs/usage.md)
- [Configuration](docs/configuration.md) - [Configuration](docs/configuration.md)
- [Development](docs/development.md) - [Development](docs/development.md)
- [Architecture](docs/architecture.md) (includes `OverlayManager` state ownership and deps wiring rules)
### Third-Party Components ### Third-Party Components

View File

@@ -2,7 +2,7 @@
* SubMiner Example Configuration File * SubMiner Example Configuration File
* *
* This file is auto-generated from src/config/definitions.ts. * This file is auto-generated from src/config/definitions.ts.
* Copy to ~/.config/SubMiner/config.jsonc and edit as needed. * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/ */
{ {
@@ -115,7 +115,8 @@
"multiCopyTimeoutMs": 3000, "multiCopyTimeoutMs": 3000,
"toggleSecondarySub": "CommandOrControl+Shift+V", "toggleSecondarySub": "CommandOrControl+Shift+V",
"markAudioCard": "CommandOrControl+Shift+A", "markAudioCard": "CommandOrControl+Shift+A",
"openRuntimeOptions": "CommandOrControl+Shift+O" "openRuntimeOptions": "CommandOrControl+Shift+O",
"openJimaku": "Ctrl+Alt+J"
}, },
// ========================================== // ==========================================

View File

@@ -28,10 +28,10 @@ pnpm run docs:build
- Full config file reference and option details - Full config file reference and option details
- [Development](/development) - [Development](/development)
- Contributor notes - Contributor notes
- Architecture migration overview - Architecture and extension rules
- Environment variables - Environment variables
- License and acknowledgments - License and acknowledgments
- [Architecture](/architecture) - [Architecture](/architecture)
- Composability migration status - Service-oriented runtime structure
- Core runtime structure - Composition and lifecycle model
- Extension design rules - Extension design rules

View File

@@ -1,51 +1,75 @@
# Architecture # Architecture
SubMiner is migrating from a single, monolithic `src/main.ts` runtime toward a composable architecture with clear extension points. SubMiner uses a service-oriented Electron main-process architecture where `src/main.ts` acts as the composition root and behavior lives in small runtime services under `src/core/services`.
## Goals
- Keep behavior stable while reducing coupling.
- Prefer small, single-purpose units that can be tested in isolation.
- Keep `main.ts` focused on wiring and state ownership, not implementation detail.
- Follow Unix-style composability:
- each service does one job
- services compose through explicit inputs/outputs
- orchestration is separate from implementation
## Current Structure ## Current Structure
- `src/main.ts`: bootstrap/composition root plus remaining legacy orchestration. - `src/main.ts`
- `src/core/`: shared runtime primitives: - Composition root for lifecycle wiring and non-overlay runtime state.
- `app-orchestrator.ts`: lifecycle wiring for ready/activate/quit hooks. - Owns long-lived process state for trackers, runtime flags, and client instances.
- `action-bus.ts`: typed action dispatch path. - Delegates behavior to services.
- `actions.ts`: canonical app action types. - `src/core/services/overlay-manager-service.ts`
- `module.ts` / `module-registry.ts`: module lifecycle contract. - Owns overlay/window state (`mainWindow`, `invisibleWindow`, visible/invisible overlay flags).
- `services/`: extracted runtime services (IPC, shortcuts, subtitle, overlay, MPV command routing). - Provides a narrow state API used by `main.ts` and overlay services.
- `src/ipc/`: shared IPC contract + wrappers used by main, preload, and renderer. - `src/core/services/*`
- `src/subtitle/`: staged subtitle pipeline (`normalize` -> `tokenize` -> `merge`). - Stateless or narrowly stateful units for a specific responsibility.
- `src/modules/`: feature modules: - Examples: startup bootstrap, app lifecycle hooks, CLI command handling, IPC registration, overlay visibility, MPV IPC behavior, shortcut registration, subtitle websocket, jimaku/subsync helpers.
- `runtime-options` - `src/core/utils/*`
- `jimaku` - Pure helpers and coercion/config utilities.
- `subsync` - `src/cli/*`
- `anki` - CLI parsing and help output.
- `texthooker` - `src/config/*`
- provider registries: - Config schema/definitions, defaults, validation, and template generation.
- `src/window-trackers/index.ts` - `src/window-trackers/*`
- `src/tokenizers/index.ts` - Backend-specific tracker implementations plus selection index.
- `src/token-mergers/index.ts` - `src/jimaku/*`, `src/subsync/*`
- `src/translators/index.ts` - Domain-specific integration helpers.
- `src/subsync/engines.ts`
## Migration Status ## Composition Pattern
- Completed: Most runtime code follows a dependency-injection pattern:
- Action bus wired for CLI, shortcuts, and IPC-triggered commands.
- Command/action mapping covered by focused core tests.
- Shared IPC channel contract adopted across main/preload/renderer.
- Runtime options extracted into module lifecycle.
- Provider registries replace hardcoded backend selection.
- Subtitle tokenization/merge/enrich flow moved to staged pipeline.
- Stage-level subtitle pipeline tests added for deterministic behavior.
- Jimaku, Subsync, Anki, and texthooker/websocket flows moduleized.
- In progress:
- Further shrink `src/main.ts` by moving orchestration into dedicated services/orchestrator files.
- Continue moduleizing remaining integrations with complex lifecycle coupling.
## Design Rules 1. Define a service interface in `src/core/services/*`.
2. Keep core logic in pure or side-effect-bounded functions.
3. Build runtime deps in `main.ts`; use `*-deps-runtime-service.ts` helpers only when they add real adaptation logic.
4. Call the service from lifecycle/command wiring points.
- New feature behavior should be added as: This keeps side effects explicit and makes behavior easy to unit-test with fakes.
- a module (`src/modules/*`) and/or
- a provider registration (`src/*/index.ts` registries), ## Lifecycle Model
instead of editing unrelated branches in `src/main.ts`.
- New command triggers should dispatch `AppAction` and reuse existing action handlers. - Startup:
- New IPC channels should be added only via `src/ipc/contract.ts`. - `startup-bootstrap-runtime-service` handles initial argv/env/backend setup and decides generate-config flow vs app lifecycle start.
- `app-lifecycle-service` handles Electron single-instance + lifecycle event registration.
- `startup-lifecycle-hooks-runtime-service` wires app-ready and app-shutdown hooks.
- Runtime:
- CLI/shortcut/IPC events map to service calls.
- Overlay and MPV state sync through dedicated services.
- Runtime options and mining flows are coordinated via service boundaries.
- Shutdown:
- `app-shutdown-runtime-service` coordinates cleanup ordering (shortcuts, sockets, trackers, integrations).
## Why This Design
- Smaller blast radius: changing one feature usually touches one service.
- Better testability: most behavior can be tested without Electron windows/mpv.
- Better reviewability: PRs can be scoped to one subsystem.
- Backward compatibility: CLI flags and IPC channels can remain stable while internals evolve.
## Extension Rules
- Add behavior to an existing service or a new `src/core/services/*` file, not as ad-hoc logic in `main.ts`.
- Keep service APIs explicit and narrowly scoped.
- Prefer additive changes that preserve existing CLI flags and IPC channel behavior.
- Add/update unit tests for each service extraction or behavior change.
- For cross-cutting changes, extract-first then refactor internals after parity is verified.

View File

@@ -1,6 +1,6 @@
# Configuration # Configuration
Settings are stored in `~/.config/SubMiner/config.jsonc` Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc` when `XDG_CONFIG_HOME` is unset). For backward compatibility, SubMiner also reads existing configs from lowercase `subminer` directories.
### Configuration File ### Configuration File

View File

@@ -4,7 +4,12 @@ To add or change a config option, update `src/config/definitions.ts` first. Defa
## Architecture ## Architecture
The composability migration state and extension-point guidelines are documented in [`architecture.md`](/architecture). The current runtime design, composition model, and extension guidelines are documented in [`architecture.md`](/architecture).
Contributor guidance:
- Overlay window/visibility state is owned by `src/core/services/overlay-manager-service.ts`.
- Prefer direct inline deps objects in `main.ts` for simple pass-through wiring.
- Add a `*-deps-runtime-service.ts` helper only when it performs meaningful adaptation (not identity mapping).
## Environment Variables ## Environment Variables

View File

@@ -34,10 +34,10 @@ features:
details: Build, test, and package SubMiner with the development notes in this docs set. details: Build, test, and package SubMiner with the development notes in this docs set.
--- ---
## Documentation Sections <!-- ## Documentation Sections
- [Installation](/installation) - [Installation](/installation)
- [Usage](/usage) - [Usage](/usage)
- [Configuration](/configuration) - [Configuration](/configuration)
- [Development](/development) - [Development](/development)
- [Architecture](/architecture) - [Architecture](/architecture) -->

View File

@@ -127,7 +127,7 @@ mpv --input-ipc-server=/tmp/subminer-socket video.mkv
**Config Location:** **Config Location:**
Settings are stored in `~/.config/SubMiner/config.jsonc` (same as Linux). Settings are stored in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`, same as Linux).
**MeCab Installation Paths:** **MeCab Installation Paths:**

View File

@@ -2,7 +2,7 @@
* SubMiner Example Configuration File * SubMiner Example Configuration File
* *
* This file is auto-generated from src/config/definitions.ts. * This file is auto-generated from src/config/definitions.ts.
* Copy to ~/.config/SubMiner/config.jsonc and edit as needed. * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/ */
{ {

View File

@@ -7,8 +7,6 @@ There are two ways to use SubMiner:
| **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. | | **subminer script** | All-in-one solution. Handles video selection, launches MPV with the correct socket, starts the overlay automatically, and cleans up on exit. |
| **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. | | **MPV plugin** | When you launch MPV yourself or from other tools. Provides in-MPV chord keybindings (e.g. `y-y` for menu) to control visible and invisible overlay layers. Requires `--input-ipc-server=/tmp/subminer-socket`. |
Jimaku modal shortcut is an overlay shortcut, not an MPV plugin chord: default `Ctrl+Alt+J` via `shortcuts.openJimaku`.
You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow. You can use both together—install the plugin for on-demand control, but use `subminer` when you want the streamlined workflow.
`subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`. `subminer` is implemented as a Bun script and runs directly via shebang (no `bun run` needed), for example: `subminer video.mkv`.
@@ -85,7 +83,7 @@ Notes:
- Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset). - Secondary target languages come from `secondarySub.secondarySubLanguages` (defaults to English if unset).
- `subminer` prefers subtitle tracks from yt-dlp first, then falls back to local `whisper.cpp` (`whisper-cli`) when tracks are missing. - `subminer` prefers subtitle tracks from yt-dlp first, then falls back to local `whisper.cpp` (`whisper-cli`) when tracks are missing.
- Whisper translation fallback currently only supports English secondary targets; non-English secondary targets rely on yt-dlp subtitle availability. - Whisper translation fallback currently only supports English secondary targets; non-English secondary targets rely on yt-dlp subtitle availability.
- Configure defaults in `~/.config/SubMiner/config.jsonc` under `youtubeSubgen` and `secondarySub`, or override mode/tool paths via CLI flags/environment variables. - Configure defaults in `$XDG_CONFIG_HOME/SubMiner/config.jsonc` (or `~/.config/SubMiner/config.jsonc`) under `youtubeSubgen` and `secondarySub`, or override mode/tool paths via CLI flags/environment variables.
## Keybindings ## Keybindings
@@ -117,12 +115,6 @@ Notes:
These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization. These keybindings only work when the overlay window has focus. See [Configuration](/configuration) for customization.
### Overlay Chord Shortcuts
| Chord | Action |
| --------- | ------------------------- |
| `y``j` | Open Jimaku subtitle menu |
## How It Works ## How It Works
1. MPV runs with an IPC socket at `/tmp/subminer-socket` 1. MPV runs with an IPC socket at `/tmp/subminer-socket`

View File

@@ -12,11 +12,11 @@
"check:main-lines:gate3": "bash scripts/check-main-lines.sh 2500", "check:main-lines:gate3": "bash scripts/check-main-lines.sh 2500",
"check:main-lines:gate4": "bash scripts/check-main-lines.sh 1800", "check:main-lines:gate4": "bash scripts/check-main-lines.sh 1800",
"check:main-lines:gate5": "bash scripts/check-main-lines.sh 1500", "check:main-lines:gate5": "bash scripts/check-main-lines.sh 1500",
"docs:dev": "vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort", "docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort",
"docs:build": "vitepress build docs", "docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort", "docs:preview": "VITE_EXTRA_EXTENSIONS=jsonc vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
"test:config": "pnpm run build && node --test dist/config/config.test.js", "test:config": "pnpm run build && node --test dist/config/config.test.js",
"test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/anki-jimaku-ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/overlay-visibility-facade-deps-runtime-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-runtime-deps-service.test.js dist/core/services/startup-lifecycle-runtime-deps-service.test.js dist/core/services/startup-lifecycle-hooks-runtime-service.test.js dist/core/services/overlay-shortcut-runtime-deps-service.test.js dist/core/services/mining-runtime-deps-service.test.js dist/core/services/shortcut-ui-runtime-deps-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/mpv-client-deps-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js", "test:core": "pnpm run build && node --test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command-service.test.js dist/core/services/cli-command-deps-runtime-service.test.js dist/core/services/ipc-deps-runtime-service.test.js dist/core/services/field-grouping-overlay-runtime-service.test.js dist/core/services/subsync-deps-runtime-service.test.js dist/core/services/numeric-shortcut-runtime-service.test.js dist/core/services/numeric-shortcut-session-service.test.js dist/core/services/mpv-command-ipc-deps-runtime-service.test.js dist/core/services/runtime-options-ipc-deps-runtime-service.test.js dist/core/services/tokenizer-deps-runtime-service.test.js dist/core/services/overlay-deps-runtime-service.test.js dist/core/services/startup-lifecycle-hooks-runtime-service.test.js dist/core/services/shortcut-ui-deps-runtime-service.test.js dist/core/services/secondary-subtitle-service.test.js dist/core/services/mpv-render-metrics-service.test.js dist/core/services/mpv-runtime-service.test.js dist/core/services/runtime-options-runtime-service.test.js dist/core/services/overlay-modal-restore-service.test.js dist/core/services/runtime-config-service.test.js dist/core/services/overlay-bridge-runtime-service.test.js dist/core/services/overlay-visibility-facade-service.test.js dist/core/services/overlay-broadcast-runtime-service.test.js dist/core/services/overlay-manager-service.test.js dist/core/services/app-ready-runtime-service.test.js dist/core/services/app-shutdown-runtime-service.test.js dist/core/services/app-lifecycle-deps-runtime-service.test.js dist/core/services/runtime-options-manager-runtime-service.test.js dist/core/services/config-warning-runtime-service.test.js dist/core/services/app-logging-runtime-service.test.js dist/core/services/startup-resource-runtime-service.test.js dist/core/services/config-generation-runtime-service.test.js",
"test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js", "test:subtitle": "pnpm run build && node --test dist/subtitle/stages.test.js dist/subtitle/pipeline.test.js",
"generate:config-example": "pnpm run build && node dist/generate-config-example.js", "generate:config-example": "pnpm run build && node dist/generate-config-example.js",
"start": "pnpm run build && electron . --start", "start": "pnpm run build && electron . --start",

View File

@@ -55,7 +55,7 @@ export function generateConfigTemplate(config: ResolvedConfig = deepCloneConfig(
lines.push(" * SubMiner Example Configuration File"); lines.push(" * SubMiner Example Configuration File");
lines.push(" *"); lines.push(" *");
lines.push(" * This file is auto-generated from src/config/definitions.ts."); lines.push(" * This file is auto-generated from src/config/definitions.ts.");
lines.push(" * Copy to ~/.config/SubMiner/config.jsonc and edit as needed."); lines.push(" * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.");
lines.push(" */"); lines.push(" */");
lines.push("{"); lines.push("{");

View File

@@ -1,48 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
AnkiJimakuIpcDepsRuntimeOptions,
createAnkiJimakuIpcDepsRuntimeService,
} from "./anki-jimaku-ipc-deps-runtime-service";
test("createAnkiJimakuIpcDepsRuntimeService returns passthrough runtime options", async () => {
const calls: string[] = [];
const options = {
patchAnkiConnectEnabled: () => calls.push("patch"),
getResolvedConfig: () => ({ ankiConnect: undefined }),
getRuntimeOptionsManager: () => null,
getSubtitleTimingTracker: () => null,
getMpvClient: () => null,
getAnkiIntegration: () => null,
setAnkiIntegration: () => calls.push("set-integration"),
showDesktopNotification: () => calls.push("notify"),
createFieldGroupingCallback: () => async () => ({
keepNoteId: 0,
deleteNoteId: 0,
deleteDuplicate: false,
cancelled: true,
}),
broadcastRuntimeOptionsChanged: () => calls.push("broadcast"),
getFieldGroupingResolver: () => null,
setFieldGroupingResolver: () => calls.push("set-resolver"),
parseMediaInfo: () => ({ mediaPath: null, baseName: null, episode: null }),
getCurrentMediaPath: () => "/tmp/a.mp4",
jimakuFetchJson: async () => ({ ok: true, data: [] }),
getJimakuMaxEntryResults: () => 100,
getJimakuLanguagePreference: () => "prefer-japanese",
resolveJimakuApiKey: async () => "abc",
isRemoteMediaPath: () => false,
downloadToFile: async () => ({ ok: true, path: "/tmp/a.srt" }),
} as unknown as AnkiJimakuIpcDepsRuntimeOptions;
const runtime = createAnkiJimakuIpcDepsRuntimeService(options);
runtime.patchAnkiConnectEnabled(true);
runtime.broadcastRuntimeOptionsChanged();
runtime.setFieldGroupingResolver(null);
assert.deepEqual(calls, ["patch", "broadcast", "set-resolver"]);
assert.equal(runtime.getCurrentMediaPath(), "/tmp/a.mp4");
assert.equal(runtime.getJimakuMaxEntryResults(), 100);
assert.equal(await runtime.resolveJimakuApiKey(), "abc");
});

View File

@@ -1,32 +0,0 @@
import {
AnkiJimakuIpcRuntimeOptions,
} from "./anki-jimaku-runtime-service";
export type AnkiJimakuIpcDepsRuntimeOptions = AnkiJimakuIpcRuntimeOptions;
export function createAnkiJimakuIpcDepsRuntimeService(
options: AnkiJimakuIpcDepsRuntimeOptions,
): AnkiJimakuIpcRuntimeOptions {
return {
patchAnkiConnectEnabled: options.patchAnkiConnectEnabled,
getResolvedConfig: options.getResolvedConfig,
getRuntimeOptionsManager: options.getRuntimeOptionsManager,
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
getMpvClient: options.getMpvClient,
getAnkiIntegration: options.getAnkiIntegration,
setAnkiIntegration: options.setAnkiIntegration,
showDesktopNotification: options.showDesktopNotification,
createFieldGroupingCallback: options.createFieldGroupingCallback,
broadcastRuntimeOptionsChanged: options.broadcastRuntimeOptionsChanged,
getFieldGroupingResolver: options.getFieldGroupingResolver,
setFieldGroupingResolver: options.setFieldGroupingResolver,
parseMediaInfo: options.parseMediaInfo,
getCurrentMediaPath: options.getCurrentMediaPath,
jimakuFetchJson: options.jimakuFetchJson,
getJimakuMaxEntryResults: options.getJimakuMaxEntryResults,
getJimakuLanguagePreference: options.getJimakuLanguagePreference,
resolveJimakuApiKey: options.resolveJimakuApiKey,
isRemoteMediaPath: options.isRemoteMediaPath,
downloadToFile: options.downloadToFile,
};
}

View File

@@ -73,15 +73,12 @@ export {
getOverlayWindowsRuntimeService, getOverlayWindowsRuntimeService,
setOverlayDebugVisualizationEnabledRuntimeService, setOverlayDebugVisualizationEnabledRuntimeService,
} from "./overlay-broadcast-runtime-service"; } from "./overlay-broadcast-runtime-service";
export { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service"; export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
export { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service"; export { createCliCommandDepsRuntimeService } from "./cli-command-deps-runtime-service";
export { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service"; export { createIpcDepsRuntimeService } from "./ipc-deps-runtime-service";
export { createAnkiJimakuIpcDepsRuntimeService } from "./anki-jimaku-ipc-deps-runtime-service";
export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service"; export { createFieldGroupingOverlayRuntimeService } from "./field-grouping-overlay-runtime-service";
export { createSubsyncRuntimeDepsService } from "./subsync-deps-runtime-service"; export { createSubsyncRuntimeDepsService } from "./subsync-deps-runtime-service";
export { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service"; export { createNumericShortcutRuntimeService } from "./numeric-shortcut-runtime-service";
export { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
export { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service"; export { createMpvCommandIpcDepsRuntimeService } from "./mpv-command-ipc-deps-runtime-service";
export { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service"; export { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service"; export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
@@ -90,26 +87,8 @@ export {
createInvisibleOverlayVisibilityDepsRuntimeService, createInvisibleOverlayVisibilityDepsRuntimeService,
createOverlayWindowRuntimeDepsService, createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService, createVisibleOverlayVisibilityDepsRuntimeService,
} from "./overlay-runtime-deps-service"; } from "./overlay-deps-runtime-service";
export { export { runOverlayShortcutLocalFallbackRuntimeService } from "./shortcut-ui-deps-runtime-service";
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./overlay-shortcut-runtime-deps-service";
export {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./mining-runtime-deps-service";
export {
createGlobalShortcutRegistrationDepsRuntimeService,
createSecondarySubtitleCycleDepsRuntimeService,
createYomitanSettingsWindowDepsRuntimeService,
runOverlayShortcutLocalFallbackRuntimeService,
} from "./shortcut-ui-runtime-deps-service";
export { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service"; export { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service";
export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service"; export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-runtime-service";
export { createAppLoggingRuntimeService } from "./app-logging-runtime-service"; export { createAppLoggingRuntimeService } from "./app-logging-runtime-service";
@@ -122,3 +101,4 @@ export { runStartupBootstrapRuntimeService } from "./startup-bootstrap-runtime-s
export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runtime-service"; export { runSubsyncManualFromIpcRuntimeService, triggerSubsyncFromConfigRuntimeService } from "./subsync-runtime-service";
export { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service"; export { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service";
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service"; export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service";
export { createOverlayManagerService } from "./overlay-manager-service";

View File

@@ -1,65 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createCopyCurrentSubtitleDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMineSentenceCardDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
} from "./mining-runtime-deps-service";
test("mining runtime deps builders preserve references", () => {
const showMpvOsd = (_text: string) => {};
const writeClipboardText = (_text: string) => {};
const readClipboardText = () => "x";
const logError = (_message: string, _err: unknown) => {};
const subtitleTimingTracker = null;
const ankiIntegration = null;
const mpvClient = null;
const multiCopy = createHandleMultiCopyDigitDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const copyCurrent = createCopyCurrentSubtitleDepsRuntimeService({
subtitleTimingTracker,
writeClipboardText,
showMpvOsd,
});
const updateLast = createUpdateLastCardFromClipboardDepsRuntimeService({
ankiIntegration,
readClipboardText,
showMpvOsd,
});
const fieldGrouping = createTriggerFieldGroupingDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const markAudio = createMarkLastCardAsAudioCardDepsRuntimeService({
ankiIntegration,
showMpvOsd,
});
const mineCard = createMineSentenceCardDepsRuntimeService({
ankiIntegration,
mpvClient,
showMpvOsd,
});
const mineDigit = createHandleMineSentenceDigitDepsRuntimeService({
subtitleTimingTracker,
ankiIntegration,
getCurrentSecondarySubText: () => undefined,
showMpvOsd,
logError,
});
assert.equal(multiCopy.writeClipboardText, writeClipboardText);
assert.equal(copyCurrent.showMpvOsd, showMpvOsd);
assert.equal(updateLast.readClipboardText, readClipboardText);
assert.equal(fieldGrouping.ankiIntegration, ankiIntegration);
assert.equal(markAudio.showMpvOsd, showMpvOsd);
assert.equal(mineCard.mpvClient, mpvClient);
assert.equal(mineDigit.logError, logError);
});

View File

@@ -1,107 +0,0 @@
import {
copyCurrentSubtitleService,
handleMineSentenceDigitService,
handleMultiCopyDigitService,
markLastCardAsAudioCardService,
mineSentenceCardService,
triggerFieldGroupingService,
updateLastCardFromClipboardService,
} from "./mining-runtime-service";
export function createHandleMultiCopyDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMultiCopyDigitService>[1]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof handleMultiCopyDigitService>[1]["writeClipboardText"];
showMpvOsd: Parameters<typeof handleMultiCopyDigitService>[1]["showMpvOsd"];
},
): Parameters<typeof handleMultiCopyDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createCopyCurrentSubtitleDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof copyCurrentSubtitleService>[0]["subtitleTimingTracker"];
writeClipboardText: Parameters<typeof copyCurrentSubtitleService>[0]["writeClipboardText"];
showMpvOsd: Parameters<typeof copyCurrentSubtitleService>[0]["showMpvOsd"];
},
): Parameters<typeof copyCurrentSubtitleService>[0] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
writeClipboardText: options.writeClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createUpdateLastCardFromClipboardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof updateLastCardFromClipboardService>[0]["ankiIntegration"];
readClipboardText: Parameters<typeof updateLastCardFromClipboardService>[0]["readClipboardText"];
showMpvOsd: Parameters<typeof updateLastCardFromClipboardService>[0]["showMpvOsd"];
},
): Parameters<typeof updateLastCardFromClipboardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
readClipboardText: options.readClipboardText,
showMpvOsd: options.showMpvOsd,
};
}
export function createTriggerFieldGroupingDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof triggerFieldGroupingService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof triggerFieldGroupingService>[0]["showMpvOsd"];
},
): Parameters<typeof triggerFieldGroupingService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMarkLastCardAsAudioCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof markLastCardAsAudioCardService>[0]["ankiIntegration"];
showMpvOsd: Parameters<typeof markLastCardAsAudioCardService>[0]["showMpvOsd"];
},
): Parameters<typeof markLastCardAsAudioCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
showMpvOsd: options.showMpvOsd,
};
}
export function createMineSentenceCardDepsRuntimeService(
options: {
ankiIntegration: Parameters<typeof mineSentenceCardService>[0]["ankiIntegration"];
mpvClient: Parameters<typeof mineSentenceCardService>[0]["mpvClient"];
showMpvOsd: Parameters<typeof mineSentenceCardService>[0]["showMpvOsd"];
},
): Parameters<typeof mineSentenceCardService>[0] {
return {
ankiIntegration: options.ankiIntegration,
mpvClient: options.mpvClient,
showMpvOsd: options.showMpvOsd,
};
}
export function createHandleMineSentenceDigitDepsRuntimeService(
options: {
subtitleTimingTracker: Parameters<typeof handleMineSentenceDigitService>[1]["subtitleTimingTracker"];
ankiIntegration: Parameters<typeof handleMineSentenceDigitService>[1]["ankiIntegration"];
getCurrentSecondarySubText: Parameters<typeof handleMineSentenceDigitService>[1]["getCurrentSecondarySubText"];
showMpvOsd: Parameters<typeof handleMineSentenceDigitService>[1]["showMpvOsd"];
logError: Parameters<typeof handleMineSentenceDigitService>[1]["logError"];
},
): Parameters<typeof handleMineSentenceDigitService>[1] {
return {
subtitleTimingTracker: options.subtitleTimingTracker,
ankiIntegration: options.ankiIntegration,
getCurrentSecondarySubText: options.getCurrentSecondarySubText,
showMpvOsd: options.showMpvOsd,
logError: options.logError,
};
}

View File

@@ -1,51 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
test("createMpvIpcClientDepsRuntimeService returns passthrough dep object", async () => {
const marker = {
getResolvedConfig: () => ({ auto_start_overlay: false } as never),
autoStartOverlay: true,
setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isVisibleOverlayVisible: () => false,
getReconnectTimer: () => null,
setReconnectTimer: () => {},
getCurrentSubText: () => "x",
setCurrentSubText: () => {},
setCurrentSubAssText: () => {},
getSubtitleTimingTracker: () => null,
subtitleWsBroadcast: () => {},
getOverlayWindowsCount: () => 0,
tokenizeSubtitle: async () => ({ text: "x", tokens: [], mergedTokens: [] }),
broadcastToOverlayWindows: () => {},
updateCurrentMediaPath: () => {},
updateMpvSubtitleRenderMetrics: () => {},
getMpvSubtitleRenderMetrics: () => ({
subPos: 100,
subFontSize: 40,
subScale: 1,
subMarginY: 0,
subMarginX: 0,
subFont: "sans",
subSpacing: 0,
subBold: false,
subItalic: false,
subBorderSize: 0,
subShadowOffset: 0,
subAssOverride: "yes",
subScaleByWindow: true,
subUseMargins: true,
osdHeight: 720,
osdDimensions: null,
}),
setPreviousSecondarySubVisibility: () => {},
showMpvOsd: () => {},
};
const deps = createMpvIpcClientDepsRuntimeService(marker);
assert.equal(deps.autoStartOverlay, true);
assert.equal(deps.getCurrentSubText(), "x");
assert.equal(deps.getOverlayWindowsCount(), 0);
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
});

View File

@@ -1,61 +0,0 @@
import {
MpvIpcClientDeps,
} from "./mpv-service";
import { Config, MpvSubtitleRenderMetrics, SubtitleData } from "../../types";
interface SubtitleTimingTrackerLike {
recordSubtitle: (text: string, start: number, end: number) => void;
}
export interface MpvClientDepsRuntimeOptions {
getResolvedConfig: () => Config;
autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
getCurrentSubText: () => string;
setCurrentSubText: (text: string) => void;
setCurrentSubAssText: (text: string) => void;
getSubtitleTimingTracker: () => SubtitleTimingTrackerLike | null;
subtitleWsBroadcast: (text: string) => void;
getOverlayWindowsCount: () => number;
tokenizeSubtitle: (text: string) => Promise<SubtitleData>;
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
updateCurrentMediaPath: (mediaPath: unknown) => void;
updateMpvSubtitleRenderMetrics: (
patch: Partial<MpvSubtitleRenderMetrics>,
) => void;
getMpvSubtitleRenderMetrics: () => MpvSubtitleRenderMetrics;
setPreviousSecondarySubVisibility: (value: boolean | null) => void;
showMpvOsd: (text: string) => void;
}
export function createMpvIpcClientDepsRuntimeService(
options: MpvClientDepsRuntimeOptions,
): MpvIpcClientDeps {
return {
getResolvedConfig: options.getResolvedConfig,
autoStartOverlay: options.autoStartOverlay,
setOverlayVisible: options.setOverlayVisible,
shouldBindVisibleOverlayToMpvSubVisibility:
options.shouldBindVisibleOverlayToMpvSubVisibility,
isVisibleOverlayVisible: options.isVisibleOverlayVisible,
getReconnectTimer: options.getReconnectTimer,
setReconnectTimer: options.setReconnectTimer,
getCurrentSubText: options.getCurrentSubText,
setCurrentSubText: options.setCurrentSubText,
setCurrentSubAssText: options.setCurrentSubAssText,
getSubtitleTimingTracker: options.getSubtitleTimingTracker,
subtitleWsBroadcast: options.subtitleWsBroadcast,
getOverlayWindowsCount: options.getOverlayWindowsCount,
tokenizeSubtitle: options.tokenizeSubtitle,
broadcastToOverlayWindows: options.broadcastToOverlayWindows,
updateCurrentMediaPath: options.updateCurrentMediaPath,
updateMpvSubtitleRenderMetrics: options.updateMpvSubtitleRenderMetrics,
getMpvSubtitleRenderMetrics: options.getMpvSubtitleRenderMetrics,
setPreviousSecondarySubVisibility: options.setPreviousSecondarySubVisibility,
showMpvOsd: options.showMpvOsd,
};
}

View File

@@ -5,7 +5,7 @@ import {
createInvisibleOverlayVisibilityDepsRuntimeService, createInvisibleOverlayVisibilityDepsRuntimeService,
createOverlayWindowRuntimeDepsService, createOverlayWindowRuntimeDepsService,
createVisibleOverlayVisibilityDepsRuntimeService, createVisibleOverlayVisibilityDepsRuntimeService,
} from "./overlay-runtime-deps-service"; } from "./overlay-deps-runtime-service";
test("createOverlayWindowRuntimeDepsService maps runtime state providers", () => { test("createOverlayWindowRuntimeDepsService maps runtime state providers", () => {
let visible = true; let visible = true;

View File

@@ -0,0 +1,42 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createOverlayManagerService } from "./overlay-manager-service";
test("overlay manager initializes with empty windows and hidden overlays", () => {
const manager = createOverlayManagerService();
assert.equal(manager.getMainWindow(), null);
assert.equal(manager.getInvisibleWindow(), null);
assert.equal(manager.getVisibleOverlayVisible(), false);
assert.equal(manager.getInvisibleOverlayVisible(), false);
assert.deepEqual(manager.getOverlayWindows(), []);
});
test("overlay manager stores window references and returns stable window order", () => {
const manager = createOverlayManagerService();
const visibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
const invisibleWindow = { isDestroyed: () => false } as unknown as Electron.BrowserWindow;
manager.setMainWindow(visibleWindow);
manager.setInvisibleWindow(invisibleWindow);
assert.equal(manager.getMainWindow(), visibleWindow);
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow]);
});
test("overlay manager excludes destroyed windows", () => {
const manager = createOverlayManagerService();
manager.setMainWindow({ isDestroyed: () => true } as unknown as Electron.BrowserWindow);
manager.setInvisibleWindow({ isDestroyed: () => false } as unknown as Electron.BrowserWindow);
assert.equal(manager.getOverlayWindows().length, 1);
});
test("overlay manager stores visibility state", () => {
const manager = createOverlayManagerService();
manager.setVisibleOverlayVisible(true);
manager.setInvisibleOverlayVisible(true);
assert.equal(manager.getVisibleOverlayVisible(), true);
assert.equal(manager.getInvisibleOverlayVisible(), true);
});

View File

@@ -0,0 +1,49 @@
import { BrowserWindow } from "electron";
export interface OverlayManagerService {
getMainWindow: () => BrowserWindow | null;
setMainWindow: (window: BrowserWindow | null) => void;
getInvisibleWindow: () => BrowserWindow | null;
setInvisibleWindow: (window: BrowserWindow | null) => void;
getVisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
getInvisibleOverlayVisible: () => boolean;
setInvisibleOverlayVisible: (visible: boolean) => void;
getOverlayWindows: () => BrowserWindow[];
}
export function createOverlayManagerService(): OverlayManagerService {
let mainWindow: BrowserWindow | null = null;
let invisibleWindow: BrowserWindow | null = null;
let visibleOverlayVisible = false;
let invisibleOverlayVisible = false;
return {
getMainWindow: () => mainWindow,
setMainWindow: (window) => {
mainWindow = window;
},
getInvisibleWindow: () => invisibleWindow,
setInvisibleWindow: (window) => {
invisibleWindow = window;
},
getVisibleOverlayVisible: () => visibleOverlayVisible,
setVisibleOverlayVisible: (visible) => {
visibleOverlayVisible = visible;
},
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
setInvisibleOverlayVisible: (visible) => {
invisibleOverlayVisible = visible;
},
getOverlayWindows: () => {
const windows: BrowserWindow[] = [];
if (mainWindow && !mainWindow.isDestroyed()) {
windows.push(mainWindow);
}
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
windows.push(invisibleWindow);
}
return windows;
},
};
}

View File

@@ -1,52 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
} from "./overlay-shortcut-runtime-deps-service";
test("createOverlayShortcutRuntimeDepsService returns callable runtime deps", async () => {
const calls: string[] = [];
const deps = createOverlayShortcutRuntimeDepsService({
showMpvOsd: () => calls.push("showMpvOsd"),
openRuntimeOptions: () => calls.push("openRuntimeOptions"),
openJimaku: () => calls.push("openJimaku"),
markAudioCard: async () => {
calls.push("markAudioCard");
},
copySubtitleMultiple: () => calls.push("copySubtitleMultiple"),
copySubtitle: () => calls.push("copySubtitle"),
toggleSecondarySub: () => calls.push("toggleSecondarySub"),
updateLastCardFromClipboard: async () => {
calls.push("updateLastCardFromClipboard");
},
triggerFieldGrouping: async () => {
calls.push("triggerFieldGrouping");
},
triggerSubsync: async () => {
calls.push("triggerSubsync");
},
mineSentence: async () => {
calls.push("mineSentence");
},
mineSentenceMultiple: () => calls.push("mineSentenceMultiple"),
});
deps.copySubtitle();
await deps.mineSentence();
deps.mineSentenceMultiple(2);
assert.deepEqual(calls, ["copySubtitle", "mineSentence", "mineSentenceMultiple"]);
});
test("createOverlayShortcutLifecycleDepsRuntimeService returns lifecycle passthrough", () => {
const deps = createOverlayShortcutLifecycleDepsRuntimeService({
getConfiguredShortcuts: () => ({ actions: [] } as never),
getOverlayHandlers: () => ({} as never),
cancelPendingMultiCopy: () => {},
cancelPendingMineSentenceMultiple: () => {},
});
assert.ok(deps.getConfiguredShortcuts());
assert.ok(deps.getOverlayHandlers());
});

View File

@@ -1,59 +0,0 @@
import {
OverlayShortcutLifecycleDeps,
} from "./overlay-shortcut-lifecycle-service";
import {
OverlayShortcutRuntimeDeps,
} from "./overlay-shortcut-runtime-service";
export interface OverlayShortcutRuntimeDepsOptions {
showMpvOsd: (text: string) => void;
openRuntimeOptions: () => void;
openJimaku: () => void;
markAudioCard: () => Promise<void>;
copySubtitleMultiple: (timeoutMs: number) => void;
copySubtitle: () => void;
toggleSecondarySub: () => void;
updateLastCardFromClipboard: () => Promise<void>;
triggerFieldGrouping: () => Promise<void>;
triggerSubsync: () => Promise<void>;
mineSentence: () => Promise<void>;
mineSentenceMultiple: (timeoutMs: number) => void;
}
export interface OverlayShortcutLifecycleDepsOptions {
getConfiguredShortcuts: OverlayShortcutLifecycleDeps["getConfiguredShortcuts"];
getOverlayHandlers: OverlayShortcutLifecycleDeps["getOverlayHandlers"];
cancelPendingMultiCopy: () => void;
cancelPendingMineSentenceMultiple: () => void;
}
export function createOverlayShortcutRuntimeDepsService(
options: OverlayShortcutRuntimeDepsOptions,
): OverlayShortcutRuntimeDeps {
return {
showMpvOsd: options.showMpvOsd,
openRuntimeOptions: options.openRuntimeOptions,
openJimaku: options.openJimaku,
markAudioCard: options.markAudioCard,
copySubtitleMultiple: options.copySubtitleMultiple,
copySubtitle: options.copySubtitle,
toggleSecondarySub: options.toggleSecondarySub,
updateLastCardFromClipboard: options.updateLastCardFromClipboard,
triggerFieldGrouping: options.triggerFieldGrouping,
triggerSubsync: options.triggerSubsync,
mineSentence: options.mineSentence,
mineSentenceMultiple: options.mineSentenceMultiple,
};
}
export function createOverlayShortcutLifecycleDepsRuntimeService(
options: OverlayShortcutLifecycleDepsOptions,
): OverlayShortcutLifecycleDeps {
return {
getConfiguredShortcuts: options.getConfiguredShortcuts,
getOverlayHandlers: options.getOverlayHandlers,
cancelPendingMultiCopy: options.cancelPendingMultiCopy,
cancelPendingMineSentenceMultiple:
options.cancelPendingMineSentenceMultiple,
};
}

View File

@@ -1,46 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createOverlayVisibilityFacadeDepsRuntimeService } from "./overlay-visibility-facade-deps-runtime-service";
test("createOverlayVisibilityFacadeDepsRuntimeService returns working deps object", () => {
let visible = false;
let invisible = true;
let mpvSubVisible: boolean | null = null;
let syncCalls = 0;
const deps = createOverlayVisibilityFacadeDepsRuntimeService({
getVisibleOverlayVisible: () => visible,
getInvisibleOverlayVisible: () => invisible,
setVisibleOverlayVisibleState: (nextVisible) => {
visible = nextVisible;
},
setInvisibleOverlayVisibleState: (nextVisible) => {
invisible = nextVisible;
},
updateVisibleOverlayVisibility: () => {},
updateInvisibleOverlayVisibility: () => {},
syncInvisibleOverlayMousePassthrough: () => {
syncCalls += 1;
},
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
isMpvConnected: () => true,
setMpvSubVisibility: (nextVisible) => {
mpvSubVisible = nextVisible;
},
});
assert.equal(deps.getVisibleOverlayVisible(), false);
assert.equal(deps.getInvisibleOverlayVisible(), true);
deps.setVisibleOverlayVisibleState(true);
deps.setInvisibleOverlayVisibleState(false);
deps.syncInvisibleOverlayMousePassthrough();
deps.setMpvSubVisibility(false);
assert.equal(visible, true);
assert.equal(invisible, false);
assert.equal(syncCalls, 1);
assert.equal(mpvSubVisible, false);
assert.equal(deps.shouldBindVisibleOverlayToMpvSubVisibility(), true);
assert.equal(deps.isMpvConnected(), true);
});

View File

@@ -1,35 +0,0 @@
import {
OverlayVisibilityFacadeDeps,
} from "./overlay-visibility-facade-service";
export interface OverlayVisibilityFacadeDepsRuntimeOptions {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisibleState: (nextVisible: boolean) => void;
setInvisibleOverlayVisibleState: (nextVisible: boolean) => void;
updateVisibleOverlayVisibility: () => void;
updateInvisibleOverlayVisibility: () => void;
syncInvisibleOverlayMousePassthrough: () => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
isMpvConnected: () => boolean;
setMpvSubVisibility: (mpvSubVisible: boolean) => void;
}
export function createOverlayVisibilityFacadeDepsRuntimeService(
options: OverlayVisibilityFacadeDepsRuntimeOptions,
): OverlayVisibilityFacadeDeps {
return {
getVisibleOverlayVisible: options.getVisibleOverlayVisible,
getInvisibleOverlayVisible: options.getInvisibleOverlayVisible,
setVisibleOverlayVisibleState: options.setVisibleOverlayVisibleState,
setInvisibleOverlayVisibleState: options.setInvisibleOverlayVisibleState,
updateVisibleOverlayVisibility: options.updateVisibleOverlayVisibility,
updateInvisibleOverlayVisibility: options.updateInvisibleOverlayVisibility,
syncInvisibleOverlayMousePassthrough:
options.syncInvisibleOverlayMousePassthrough,
shouldBindVisibleOverlayToMpvSubVisibility:
options.shouldBindVisibleOverlayToMpvSubVisibility,
isMpvConnected: options.isMpvConnected,
setMpvSubVisibility: options.setMpvSubVisibility,
};
}

View File

@@ -1,35 +1,11 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
createGlobalShortcutRegistrationDepsRuntimeService,
createSecondarySubtitleCycleDepsRuntimeService,
createYomitanSettingsWindowDepsRuntimeService,
runOverlayShortcutLocalFallbackRuntimeService, runOverlayShortcutLocalFallbackRuntimeService,
} from "./shortcut-ui-runtime-deps-service"; } from "./shortcut-ui-deps-runtime-service";
function makeOptions() { function makeOptions() {
return { return {
yomitanExt: null,
getYomitanSettingsWindow: () => null,
setYomitanSettingsWindow: () => {},
shortcuts: {
toggleVisibleOverlayGlobal: "Ctrl+Shift+O",
toggleInvisibleOverlayGlobal: "Ctrl+Alt+O",
},
onToggleVisibleOverlay: () => {},
onToggleInvisibleOverlay: () => {},
onOpenYomitanSettings: () => {},
isDev: false,
getMainWindow: () => null,
getSecondarySubMode: () => "hover" as const,
setSecondarySubMode: () => {},
getLastSecondarySubToggleAtMs: () => 0,
setLastSecondarySubToggleAtMs: () => {},
broadcastSecondarySubMode: () => {},
showMpvOsd: () => {},
getConfiguredShortcuts: () => ({ getConfiguredShortcuts: () => ({
toggleVisibleOverlayGlobal: null, toggleVisibleOverlayGlobal: null,
toggleInvisibleOverlayGlobal: null, toggleInvisibleOverlayGlobal: null,
@@ -63,17 +39,6 @@ function makeOptions() {
}; };
} }
test("shortcut ui deps builders return expected adapters", () => {
const options = makeOptions();
const yomitan = createYomitanSettingsWindowDepsRuntimeService(options);
const globalShortcuts = createGlobalShortcutRegistrationDepsRuntimeService(options);
const secondary = createSecondarySubtitleCycleDepsRuntimeService(options);
assert.equal(yomitan.yomitanExt, null);
assert.equal(typeof globalShortcuts.onOpenYomitanSettings, "function");
assert.equal(secondary.getSecondarySubMode(), "hover");
});
test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => { test("runOverlayShortcutLocalFallbackRuntimeService delegates and returns boolean", () => {
const options = { const options = {
...makeOptions(), ...makeOptions(),

View File

@@ -0,0 +1,24 @@
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
export interface ShortcutUiRuntimeDepsOptions {
getConfiguredShortcuts: () => ConfiguredShortcuts;
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
shortcutMatcher: (
input: Electron.Input,
accelerator: string,
allowWhenRegistered?: boolean,
) => boolean;
}
export function runOverlayShortcutLocalFallbackRuntimeService(
input: Electron.Input,
options: ShortcutUiRuntimeDepsOptions,
): boolean {
return runOverlayShortcutLocalFallback(
input,
options.getConfiguredShortcuts(),
options.shortcutMatcher,
options.getOverlayShortcutFallbackHandlers(),
);
}

View File

@@ -1,83 +0,0 @@
import { Extension } from "electron";
import { SecondarySubMode } from "../../types";
import { ConfiguredShortcuts } from "../utils/shortcut-config";
import { CycleSecondarySubModeDeps } from "./secondary-subtitle-service";
import { OverlayShortcutFallbackHandlers, runOverlayShortcutLocalFallback } from "./overlay-shortcut-fallback-runner";
import { OpenYomitanSettingsWindowOptions } from "./yomitan-settings-service";
import { RegisterGlobalShortcutsServiceOptions } from "./shortcut-service";
export interface ShortcutUiRuntimeDepsOptions {
yomitanExt: Extension | null;
getYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["getExistingWindow"];
setYomitanSettingsWindow: OpenYomitanSettingsWindowOptions["setWindow"];
shortcuts: RegisterGlobalShortcutsServiceOptions["shortcuts"];
onToggleVisibleOverlay: () => void;
onToggleInvisibleOverlay: () => void;
onOpenYomitanSettings: () => void;
isDev: boolean;
getMainWindow: RegisterGlobalShortcutsServiceOptions["getMainWindow"];
getSecondarySubMode: () => SecondarySubMode;
setSecondarySubMode: (mode: SecondarySubMode) => void;
getLastSecondarySubToggleAtMs: () => number;
setLastSecondarySubToggleAtMs: (timestampMs: number) => void;
broadcastSecondarySubMode: (mode: SecondarySubMode) => void;
showMpvOsd: (text: string) => void;
getConfiguredShortcuts: () => ConfiguredShortcuts;
getOverlayShortcutFallbackHandlers: () => OverlayShortcutFallbackHandlers;
shortcutMatcher: (
input: Electron.Input,
accelerator: string,
allowWhenRegistered?: boolean,
) => boolean;
}
export function createYomitanSettingsWindowDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): OpenYomitanSettingsWindowOptions {
return {
yomitanExt: options.yomitanExt,
getExistingWindow: options.getYomitanSettingsWindow,
setWindow: options.setYomitanSettingsWindow,
};
}
export function createGlobalShortcutRegistrationDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): RegisterGlobalShortcutsServiceOptions {
return {
shortcuts: options.shortcuts,
onToggleVisibleOverlay: options.onToggleVisibleOverlay,
onToggleInvisibleOverlay: options.onToggleInvisibleOverlay,
onOpenYomitanSettings: options.onOpenYomitanSettings,
isDev: options.isDev,
getMainWindow: options.getMainWindow,
};
}
export function createSecondarySubtitleCycleDepsRuntimeService(
options: ShortcutUiRuntimeDepsOptions,
): CycleSecondarySubModeDeps {
return {
getSecondarySubMode: options.getSecondarySubMode,
setSecondarySubMode: options.setSecondarySubMode,
getLastSecondarySubToggleAtMs: options.getLastSecondarySubToggleAtMs,
setLastSecondarySubToggleAtMs: options.setLastSecondarySubToggleAtMs,
broadcastSecondarySubMode: options.broadcastSecondarySubMode,
showMpvOsd: options.showMpvOsd,
};
}
export function runOverlayShortcutLocalFallbackRuntimeService(
input: Electron.Input,
options: ShortcutUiRuntimeDepsOptions,
): boolean {
return runOverlayShortcutLocalFallback(
input,
options.getConfiguredShortcuts(),
options.shortcutMatcher,
options.getOverlayShortcutFallbackHandlers(),
);
}

View File

@@ -7,10 +7,6 @@ import {
AppShutdownRuntimeDeps, AppShutdownRuntimeDeps,
runAppShutdownRuntimeService, runAppShutdownRuntimeService,
} from "./app-shutdown-runtime-service"; } from "./app-shutdown-runtime-service";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./startup-lifecycle-runtime-deps-service";
type StartupLifecycleHookDeps = Pick< type StartupLifecycleHookDeps = Pick<
AppLifecycleDepsRuntimeOptions, AppLifecycleDepsRuntimeOptions,
@@ -29,14 +25,10 @@ export function createStartupLifecycleHooksRuntimeService(
): StartupLifecycleHookDeps { ): StartupLifecycleHookDeps {
return { return {
onReady: async () => { onReady: async () => {
await runAppReadyRuntimeService( await runAppReadyRuntimeService(options.appReadyDeps);
createStartupAppReadyDepsRuntimeService(options.appReadyDeps),
);
}, },
onWillQuitCleanup: () => { onWillQuitCleanup: () => {
runAppShutdownRuntimeService( runAppShutdownRuntimeService(options.appShutdownDeps);
createStartupAppShutdownDepsRuntimeService(options.appShutdownDeps),
);
}, },
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate, shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
restoreWindowsOnActivate: options.restoreWindowsOnActivate, restoreWindowsOnActivate: options.restoreWindowsOnActivate,

View File

@@ -1,74 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
createStartupAppReadyDepsRuntimeService,
createStartupAppShutdownDepsRuntimeService,
} from "./startup-lifecycle-runtime-deps-service";
test("createStartupAppReadyDepsRuntimeService preserves runtime deps behavior", async () => {
const calls: string[] = [];
const deps = createStartupAppReadyDepsRuntimeService({
loadSubtitlePosition: () => calls.push("loadSubtitlePosition"),
resolveKeybindings: () => calls.push("resolveKeybindings"),
createMpvClient: () => calls.push("createMpvClient"),
reloadConfig: () => calls.push("reloadConfig"),
getResolvedConfig: () => ({
secondarySub: { defaultMode: "hover" },
websocket: { enabled: "auto", port: 1234 },
}),
getConfigWarnings: () => [],
logConfigWarning: () => {},
initRuntimeOptionsManager: () => calls.push("initRuntimeOptionsManager"),
setSecondarySubMode: () => calls.push("setSecondarySubMode"),
defaultSecondarySubMode: "hover",
defaultWebsocketPort: 8765,
hasMpvWebsocketPlugin: () => true,
startSubtitleWebsocket: () => calls.push("startSubtitleWebsocket"),
log: () => calls.push("log"),
createMecabTokenizerAndCheck: async () => {
calls.push("createMecab");
},
createSubtitleTimingTracker: () => calls.push("createSubtitleTimingTracker"),
loadYomitanExtension: async () => {
calls.push("loadYomitan");
},
texthookerOnlyMode: false,
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
initializeOverlayRuntime: () => calls.push("initOverlayRuntime"),
handleInitialArgs: () => calls.push("handleInitialArgs"),
});
deps.loadSubtitlePosition();
await deps.createMecabTokenizerAndCheck();
deps.handleInitialArgs();
assert.equal(deps.defaultWebsocketPort, 8765);
assert.equal(deps.defaultSecondarySubMode, "hover");
assert.deepEqual(calls, ["loadSubtitlePosition", "createMecab", "handleInitialArgs"]);
});
test("createStartupAppShutdownDepsRuntimeService preserves shutdown handlers", () => {
const calls: string[] = [];
const deps = createStartupAppShutdownDepsRuntimeService({
unregisterAllGlobalShortcuts: () => calls.push("unregisterAllGlobalShortcuts"),
stopSubtitleWebsocket: () => calls.push("stopSubtitleWebsocket"),
stopTexthookerService: () => calls.push("stopTexthookerService"),
destroyYomitanParserWindow: () => calls.push("destroyYomitanParserWindow"),
clearYomitanParserPromises: () => calls.push("clearYomitanParserPromises"),
stopWindowTracker: () => calls.push("stopWindowTracker"),
destroyMpvSocket: () => calls.push("destroyMpvSocket"),
clearReconnectTimer: () => calls.push("clearReconnectTimer"),
destroySubtitleTimingTracker: () => calls.push("destroySubtitleTimingTracker"),
destroyAnkiIntegration: () => calls.push("destroyAnkiIntegration"),
});
deps.stopSubtitleWebsocket();
deps.clearReconnectTimer();
deps.destroyAnkiIntegration();
assert.deepEqual(calls, [
"stopSubtitleWebsocket",
"clearReconnectTimer",
"destroyAnkiIntegration",
]);
});

View File

@@ -1,55 +0,0 @@
import {
AppReadyRuntimeDeps,
} from "./app-ready-runtime-service";
import {
AppShutdownRuntimeDeps,
} from "./app-shutdown-runtime-service";
export type StartupAppReadyDepsRuntimeOptions = AppReadyRuntimeDeps;
export type StartupAppShutdownDepsRuntimeOptions = AppShutdownRuntimeDeps;
export function createStartupAppReadyDepsRuntimeService(
options: StartupAppReadyDepsRuntimeOptions,
): AppReadyRuntimeDeps {
return {
loadSubtitlePosition: options.loadSubtitlePosition,
resolveKeybindings: options.resolveKeybindings,
createMpvClient: options.createMpvClient,
reloadConfig: options.reloadConfig,
getResolvedConfig: options.getResolvedConfig,
getConfigWarnings: options.getConfigWarnings,
logConfigWarning: options.logConfigWarning,
initRuntimeOptionsManager: options.initRuntimeOptionsManager,
setSecondarySubMode: options.setSecondarySubMode,
defaultSecondarySubMode: options.defaultSecondarySubMode,
defaultWebsocketPort: options.defaultWebsocketPort,
hasMpvWebsocketPlugin: options.hasMpvWebsocketPlugin,
startSubtitleWebsocket: options.startSubtitleWebsocket,
log: options.log,
createMecabTokenizerAndCheck: options.createMecabTokenizerAndCheck,
createSubtitleTimingTracker: options.createSubtitleTimingTracker,
loadYomitanExtension: options.loadYomitanExtension,
texthookerOnlyMode: options.texthookerOnlyMode,
shouldAutoInitializeOverlayRuntimeFromConfig:
options.shouldAutoInitializeOverlayRuntimeFromConfig,
initializeOverlayRuntime: options.initializeOverlayRuntime,
handleInitialArgs: options.handleInitialArgs,
};
}
export function createStartupAppShutdownDepsRuntimeService(
options: StartupAppShutdownDepsRuntimeOptions,
): AppShutdownRuntimeDeps {
return {
unregisterAllGlobalShortcuts: options.unregisterAllGlobalShortcuts,
stopSubtitleWebsocket: options.stopSubtitleWebsocket,
stopTexthookerService: options.stopTexthookerService,
destroyYomitanParserWindow: options.destroyYomitanParserWindow,
clearYomitanParserPromises: options.clearYomitanParserPromises,
stopWindowTracker: options.stopWindowTracker,
destroyMpvSocket: options.destroyMpvSocket,
clearReconnectTimer: options.clearReconnectTimer,
destroySubtitleTimingTracker: options.destroySubtitleTimingTracker,
destroyAnkiIntegration: options.destroyAnkiIntegration,
};
}

View File

@@ -22,7 +22,6 @@ import {
clipboard, clipboard,
shell, shell,
protocol, protocol,
screen,
Extension, Extension,
} from "electron"; } from "electron";
@@ -40,18 +39,12 @@ protocol.registerSchemesAsPrivileged([
]); ]);
import * as path from "path"; import * as path from "path";
import * as http from "http";
import * as https from "https";
import * as os from "os"; import * as os from "os";
import * as fs from "fs"; import * as fs from "fs";
import * as crypto from "crypto";
import { MecabTokenizer } from "./mecab-tokenizer"; import { MecabTokenizer } from "./mecab-tokenizer";
import { BaseWindowTracker } from "./window-trackers"; import { BaseWindowTracker } from "./window-trackers";
import type { import type {
JimakuApiResponse, JimakuApiResponse,
JimakuDownloadResult,
JimakuMediaInfo,
JimakuConfig,
JimakuLanguagePreference, JimakuLanguagePreference,
SubtitleData, SubtitleData,
SubtitlePosition, SubtitlePosition,
@@ -78,16 +71,12 @@ import {
getSubsyncConfig, getSubsyncConfig,
} from "./subsync/utils"; } from "./subsync/utils";
import { import {
hasExplicitCommand,
parseArgs, parseArgs,
shouldStartApp, shouldStartApp,
} from "./cli/args"; } from "./cli/args";
import type { CliArgs, CliCommandSource } from "./cli/args"; import type { CliArgs, CliCommandSource } from "./cli/args";
import { printHelp } from "./cli/help"; import { printHelp } from "./cli/help";
import { import {
asBoolean,
asFiniteNumber,
asString,
enforceUnsupportedWaylandMode, enforceUnsupportedWaylandMode,
forceX11Backend, forceX11Backend,
generateDefaultConfigFile, generateDefaultConfigFile,
@@ -104,48 +93,33 @@ import {
broadcastRuntimeOptionsChangedRuntimeService, broadcastRuntimeOptionsChangedRuntimeService,
broadcastToOverlayWindowsRuntimeService, broadcastToOverlayWindowsRuntimeService,
copyCurrentSubtitleService, copyCurrentSubtitleService,
createAnkiJimakuIpcDepsRuntimeService,
createAppLifecycleDepsRuntimeService, createAppLifecycleDepsRuntimeService,
createAppLoggingRuntimeService, createAppLoggingRuntimeService,
createCliCommandDepsRuntimeService, createCliCommandDepsRuntimeService,
createCopyCurrentSubtitleDepsRuntimeService, createOverlayManagerService,
createFieldGroupingOverlayRuntimeService, createFieldGroupingOverlayRuntimeService,
createGlobalShortcutRegistrationDepsRuntimeService,
createHandleMineSentenceDigitDepsRuntimeService,
createHandleMultiCopyDigitDepsRuntimeService,
createInitializeOverlayRuntimeDepsService, createInitializeOverlayRuntimeDepsService,
createInvisibleOverlayVisibilityDepsRuntimeService, createInvisibleOverlayVisibilityDepsRuntimeService,
createIpcDepsRuntimeService, createIpcDepsRuntimeService,
createMarkLastCardAsAudioCardDepsRuntimeService,
createMecabTokenizerAndCheckRuntimeService, createMecabTokenizerAndCheckRuntimeService,
createMineSentenceCardDepsRuntimeService,
createMpvCommandIpcDepsRuntimeService, createMpvCommandIpcDepsRuntimeService,
createMpvIpcClientDepsRuntimeService,
createNumericShortcutRuntimeService, createNumericShortcutRuntimeService,
createOverlayShortcutLifecycleDepsRuntimeService,
createOverlayShortcutRuntimeDepsService,
createOverlayShortcutRuntimeHandlers, createOverlayShortcutRuntimeHandlers,
createOverlayVisibilityFacadeDepsRuntimeService,
createOverlayWindowRuntimeDepsService, createOverlayWindowRuntimeDepsService,
createOverlayWindowService, createOverlayWindowService,
createRuntimeOptionsIpcDepsRuntimeService, createRuntimeOptionsIpcDepsRuntimeService,
createRuntimeOptionsManagerRuntimeService, createRuntimeOptionsManagerRuntimeService,
createSecondarySubtitleCycleDepsRuntimeService,
createStartupLifecycleHooksRuntimeService, createStartupLifecycleHooksRuntimeService,
createSubsyncRuntimeDepsService, createSubsyncRuntimeDepsService,
createSubtitleTimingTrackerRuntimeService, createSubtitleTimingTrackerRuntimeService,
createTokenizerDepsRuntimeService, createTokenizerDepsRuntimeService,
createTriggerFieldGroupingDepsRuntimeService,
createUpdateLastCardFromClipboardDepsRuntimeService,
createVisibleOverlayVisibilityDepsRuntimeService, createVisibleOverlayVisibilityDepsRuntimeService,
createYomitanSettingsWindowDepsRuntimeService,
cycleSecondarySubModeService, cycleSecondarySubModeService,
enforceOverlayLayerOrderService, enforceOverlayLayerOrderService,
ensureOverlayWindowLevelService, ensureOverlayWindowLevelService,
getInitialInvisibleOverlayVisibilityService, getInitialInvisibleOverlayVisibilityService,
getJimakuLanguagePreferenceService, getJimakuLanguagePreferenceService,
getJimakuMaxEntryResultsService, getJimakuMaxEntryResultsService,
getOverlayWindowsRuntimeService,
handleCliCommandService, handleCliCommandService,
handleMineSentenceDigitService, handleMineSentenceDigitService,
handleMpvCommandFromIpcService, handleMpvCommandFromIpcService,
@@ -154,7 +128,6 @@ import {
hasMpvWebsocketPlugin, hasMpvWebsocketPlugin,
initializeOverlayRuntimeService, initializeOverlayRuntimeService,
isAutoUpdateEnabledRuntimeService, isAutoUpdateEnabledRuntimeService,
isGlobalShortcutRegisteredSafe,
jimakuFetchJsonService, jimakuFetchJsonService,
loadSubtitlePositionService, loadSubtitlePositionService,
loadYomitanExtensionService, loadYomitanExtensionService,
@@ -211,7 +184,41 @@ if (process.platform === "linux") {
} }
const DEFAULT_TEXTHOOKER_PORT = 5174; const DEFAULT_TEXTHOOKER_PORT = 5174;
const CONFIG_DIR = path.join(os.homedir(), ".config", "SubMiner"); function resolveConfigDir(): string {
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
const baseDirs = Array.from(
new Set([
xdgConfigHome || path.join(os.homedir(), ".config"),
path.join(os.homedir(), ".config"),
]),
);
const appNames = ["SubMiner", "subminer"];
for (const baseDir of baseDirs) {
for (const appName of appNames) {
const dir = path.join(baseDir, appName);
if (
fs.existsSync(path.join(dir, "config.jsonc")) ||
fs.existsSync(path.join(dir, "config.json"))
) {
return dir;
}
}
}
for (const baseDir of baseDirs) {
for (const appName of appNames) {
const dir = path.join(baseDir, appName);
if (fs.existsSync(dir)) {
return dir;
}
}
}
return path.join(baseDirs[0], "SubMiner");
}
const CONFIG_DIR = resolveConfigDir();
const USER_DATA_PATH = CONFIG_DIR; const USER_DATA_PATH = CONFIG_DIR;
const configService = new ConfigService(CONFIG_DIR); const configService = new ConfigService(CONFIG_DIR);
const isDev = const isDev =
@@ -239,8 +246,6 @@ process.on("SIGTERM", () => {
app.quit(); app.quit();
}); });
let mainWindow: BrowserWindow | null = null;
let invisibleWindow: BrowserWindow | null = null;
let yomitanExt: Extension | null = null; let yomitanExt: Extension | null = null;
let yomitanSettingsWindow: BrowserWindow | null = null; let yomitanSettingsWindow: BrowserWindow | null = null;
let yomitanParserWindow: BrowserWindow | null = null; let yomitanParserWindow: BrowserWindow | null = null;
@@ -250,8 +255,6 @@ let mpvClient: MpvIpcClient | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let currentSubText = ""; let currentSubText = "";
let currentSubAssText = ""; let currentSubAssText = "";
let visibleOverlayVisible = false;
let invisibleOverlayVisible = false;
let windowTracker: BaseWindowTracker | null = null; let windowTracker: BaseWindowTracker | null = null;
let subtitlePosition: SubtitlePosition | null = null; let subtitlePosition: SubtitlePosition | null = null;
let currentMediaPath: string | null = null; let currentMediaPath: string | null = null;
@@ -292,12 +295,13 @@ let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
let runtimeOptionsManager: RuntimeOptionsManager | null = null; let runtimeOptionsManager: RuntimeOptionsManager | null = null;
let trackerNotReadyWarningShown = false; let trackerNotReadyWarningShown = false;
let overlayDebugVisualizationEnabled = false; let overlayDebugVisualizationEnabled = false;
const overlayManager = createOverlayManagerService();
type OverlayHostedModal = "runtime-options" | "subsync"; type OverlayHostedModal = "runtime-options" | "subsync";
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>(); const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
getMainWindow: () => mainWindow, getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisible: () => visibleOverlayVisible, getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => invisibleOverlayVisible, getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible), setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
getResolver: () => fieldGroupingResolver, getResolver: () => fieldGroupingResolver,
@@ -315,7 +319,7 @@ const SUBTITLE_POSITIONS_DIR = path.join(CONFIG_DIR, "subtitle-positions");
function getRuntimeOptionsState(): RuntimeOptionState[] { if (!runtimeOptionsManager) return []; return runtimeOptionsManager.listOptions(); } function getRuntimeOptionsState(): RuntimeOptionState[] { if (!runtimeOptionsManager) return []; return runtimeOptionsManager.listOptions(); }
function getOverlayWindows(): BrowserWindow[] { function getOverlayWindows(): BrowserWindow[] {
return getOverlayWindowsRuntimeService({ mainWindow, invisibleWindow }); return overlayManager.getOverlayWindows();
} }
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void { function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
@@ -492,13 +496,14 @@ const startupState = runStartupBootstrapRuntimeService({
createMpvClient: () => { createMpvClient: () => {
mpvClient = new MpvIpcClient( mpvClient = new MpvIpcClient(
mpvSocketPath, mpvSocketPath,
createMpvIpcClientDepsRuntimeService({ {
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
autoStartOverlay, autoStartOverlay,
setOverlayVisible: (visible) => setOverlayVisible(visible), setOverlayVisible: (visible) => setOverlayVisible(visible),
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility: () =>
shouldBindVisibleOverlayToMpvSubVisibility(), shouldBindVisibleOverlayToMpvSubVisibility(),
isVisibleOverlayVisible: () => visibleOverlayVisible, isVisibleOverlayVisible: () =>
overlayManager.getVisibleOverlayVisible(),
getReconnectTimer: () => reconnectTimer, getReconnectTimer: () => reconnectTimer,
setReconnectTimer: (timer) => { setReconnectTimer: (timer) => {
reconnectTimer = timer; reconnectTimer = timer;
@@ -532,11 +537,12 @@ const startupState = runStartupBootstrapRuntimeService({
showMpvOsd: (text) => { showMpvOsd: (text) => {
showMpvOsd(text); showMpvOsd(text);
}, },
}), },
); );
}, },
reloadConfig: () => { reloadConfig: () => {
configService.reloadConfig(); configService.reloadConfig();
appLogger.logInfo(`Using config file: ${configService.getConfigPath()}`);
}, },
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
getConfigWarnings: () => configService.getWarnings(), getConfigWarnings: () => configService.getWarnings(),
@@ -705,7 +711,7 @@ function handleCliCommand(
}, },
app: { app: {
stop: () => app.quit(), stop: () => app.quit(),
hasMainWindow: () => Boolean(mainWindow), hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
}, },
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs, getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
schedule: (fn, delayMs) => setTimeout(fn, delayMs), schedule: (fn, delayMs) => setTimeout(fn, delayMs),
@@ -773,10 +779,10 @@ function ensureOverlayWindowLevel(window: BrowserWindow): void {
function enforceOverlayLayerOrder(): void { function enforceOverlayLayerOrder(): void {
enforceOverlayLayerOrderService({ enforceOverlayLayerOrderService({
visibleOverlayVisible, visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
invisibleOverlayVisible, invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
mainWindow, mainWindow: overlayManager.getMainWindow(),
invisibleWindow, invisibleWindow: overlayManager.getInvisibleWindow(),
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
}); });
} }
@@ -810,23 +816,31 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(), onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled: (enabled) =>
setOverlayDebugVisualizationEnabled(enabled), setOverlayDebugVisualizationEnabled(enabled),
getVisibleOverlayVisible: () => visibleOverlayVisible, getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => invisibleOverlayVisible, getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
tryHandleOverlayShortcutLocalFallback: (input) => tryHandleOverlayShortcutLocalFallback: (input) =>
tryHandleOverlayShortcutLocalFallback(input), tryHandleOverlayShortcutLocalFallback(input),
onWindowClosed: (windowKind) => { onWindowClosed: (windowKind) => {
if (windowKind === "visible") { if (windowKind === "visible") {
mainWindow = null; overlayManager.setMainWindow(null);
} else { } else {
invisibleWindow = null; overlayManager.setInvisibleWindow(null);
} }
}, },
}), }),
); );
} }
function createMainWindow(): BrowserWindow { mainWindow = createOverlayWindow("visible"); return mainWindow; } function createMainWindow(): BrowserWindow {
function createInvisibleWindow(): BrowserWindow { invisibleWindow = createOverlayWindow("invisible"); return invisibleWindow; } const window = createOverlayWindow("visible");
overlayManager.setMainWindow(window);
return window;
}
function createInvisibleWindow(): BrowserWindow {
const window = createOverlayWindow("invisible");
overlayManager.setInvisibleWindow(window);
return window;
}
function initializeOverlayRuntime(): void { function initializeOverlayRuntime(): void {
if (overlayRuntimeInitialized) { if (overlayRuntimeInitialized) {
@@ -849,8 +863,9 @@ function initializeOverlayRuntime(): void {
updateOverlayBounds: (geometry) => { updateOverlayBounds: (geometry) => {
updateOverlayBounds(geometry); updateOverlayBounds(geometry);
}, },
isVisibleOverlayVisible: () => visibleOverlayVisible, isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
isInvisibleOverlayVisible: () => invisibleOverlayVisible, isInvisibleOverlayVisible: () =>
overlayManager.getInvisibleOverlayVisible(),
updateVisibleOverlayVisibility: () => { updateVisibleOverlayVisibility: () => {
updateVisibleOverlayVisibility(); updateVisibleOverlayVisibility();
}, },
@@ -875,35 +890,12 @@ function initializeOverlayRuntime(): void {
createFieldGroupingCallback: () => createFieldGroupingCallback(), createFieldGroupingCallback: () => createFieldGroupingCallback(),
}), }),
); );
invisibleOverlayVisible = result.invisibleOverlayVisible; overlayManager.setInvisibleOverlayVisible(result.invisibleOverlayVisible);
overlayRuntimeInitialized = true; overlayRuntimeInitialized = true;
} }
function getShortcutUiRuntimeDeps() { function getShortcutUiRuntimeDeps() {
return { return {
yomitanExt,
getYomitanSettingsWindow: () => yomitanSettingsWindow,
setYomitanSettingsWindow: (window: BrowserWindow | null) => {
yomitanSettingsWindow = window;
},
shortcuts: getConfiguredShortcuts(),
onToggleVisibleOverlay: () => toggleVisibleOverlay(),
onToggleInvisibleOverlay: () => toggleInvisibleOverlay(),
onOpenYomitanSettings: () => openYomitanSettings(),
isDev,
getMainWindow: () => mainWindow,
getSecondarySubMode: () => secondarySubMode,
setSecondarySubMode: (mode: SecondarySubMode) => {
secondarySubMode = mode;
},
getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs,
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
lastSecondarySubToggleAtMs = timestampMs;
},
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
broadcastToOverlayWindows("secondary-subtitle:mode", mode);
},
showMpvOsd: (text: string) => showMpvOsd(text),
getConfiguredShortcuts: () => getConfiguredShortcuts(), getConfiguredShortcuts: () => getConfiguredShortcuts(),
getOverlayShortcutFallbackHandlers: () => getOverlayShortcutFallbackHandlers: () =>
getOverlayShortcutRuntimeHandlers().fallbackHandlers, getOverlayShortcutRuntimeHandlers().fallbackHandlers,
@@ -913,12 +905,25 @@ function getShortcutUiRuntimeDeps() {
function openYomitanSettings(): void { function openYomitanSettings(): void {
openYomitanSettingsWindow( openYomitanSettingsWindow(
createYomitanSettingsWindowDepsRuntimeService(getShortcutUiRuntimeDeps()), {
yomitanExt,
getExistingWindow: () => yomitanSettingsWindow,
setWindow: (window: BrowserWindow | null) => {
yomitanSettingsWindow = window;
},
},
); );
} }
function registerGlobalShortcuts(): void { function registerGlobalShortcuts(): void {
registerGlobalShortcutsService( registerGlobalShortcutsService(
createGlobalShortcutRegistrationDepsRuntimeService(getShortcutUiRuntimeDeps()), {
shortcuts: getConfiguredShortcuts(),
onToggleVisibleOverlay: () => toggleVisibleOverlay(),
onToggleInvisibleOverlay: () => toggleInvisibleOverlay(),
onOpenYomitanSettings: () => openYomitanSettings(),
isDev,
getMainWindow: () => overlayManager.getMainWindow(),
},
); );
} }
@@ -926,7 +931,7 @@ function getConfiguredShortcuts() { return resolveConfiguredShortcuts(getResolve
function getOverlayShortcutRuntimeHandlers() { function getOverlayShortcutRuntimeHandlers() {
return createOverlayShortcutRuntimeHandlers( return createOverlayShortcutRuntimeHandlers(
createOverlayShortcutRuntimeDepsService({ {
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
openRuntimeOptions: () => { openRuntimeOptions: () => {
openRuntimeOptionsPalette(); openRuntimeOptionsPalette();
@@ -949,7 +954,7 @@ function getOverlayShortcutRuntimeHandlers() {
mineSentenceMultiple: (timeoutMs) => { mineSentenceMultiple: (timeoutMs) => {
startPendingMineSentenceMultiple(timeoutMs); startPendingMineSentenceMultiple(timeoutMs);
}, },
}), },
); );
} }
@@ -962,7 +967,20 @@ function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
function cycleSecondarySubMode(): void { function cycleSecondarySubMode(): void {
cycleSecondarySubModeService( cycleSecondarySubModeService(
createSecondarySubtitleCycleDepsRuntimeService(getShortcutUiRuntimeDeps()), {
getSecondarySubMode: () => secondarySubMode,
setSecondarySubMode: (mode: SecondarySubMode) => {
secondarySubMode = mode;
},
getLastSecondarySubToggleAtMs: () => lastSecondarySubToggleAtMs,
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
lastSecondarySubToggleAtMs = timestampMs;
},
broadcastSecondarySubMode: (mode: SecondarySubMode) => {
broadcastToOverlayWindows("secondary-subtitle:mode", mode);
},
showMpvOsd: (text: string) => showMpvOsd(text),
},
); );
} }
@@ -984,27 +1002,26 @@ const numericShortcutRuntime = createNumericShortcutRuntimeService({
}); });
const multiCopySession = numericShortcutRuntime.createSession(); const multiCopySession = numericShortcutRuntime.createSession();
const mineSentenceSession = numericShortcutRuntime.createSession(); const mineSentenceSession = numericShortcutRuntime.createSession();
const overlayVisibilityFacadeDeps = const overlayVisibilityFacadeDeps = {
createOverlayVisibilityFacadeDepsRuntimeService({ getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getVisibleOverlayVisible: () => visibleOverlayVisible, getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
getInvisibleOverlayVisible: () => invisibleOverlayVisible, setVisibleOverlayVisibleState: (nextVisible: boolean) => {
setVisibleOverlayVisibleState: (nextVisible: boolean) => { overlayManager.setVisibleOverlayVisible(nextVisible);
visibleOverlayVisible = nextVisible; },
}, setInvisibleOverlayVisibleState: (nextVisible: boolean) => {
setInvisibleOverlayVisibleState: (nextVisible: boolean) => { overlayManager.setInvisibleOverlayVisible(nextVisible);
invisibleOverlayVisible = nextVisible; },
}, updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(), updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(), syncInvisibleOverlayMousePassthrough: () =>
syncInvisibleOverlayMousePassthrough: () => syncInvisibleOverlayMousePassthrough(),
syncInvisibleOverlayMousePassthrough(), shouldBindVisibleOverlayToMpvSubVisibility: () =>
shouldBindVisibleOverlayToMpvSubVisibility: () => shouldBindVisibleOverlayToMpvSubVisibility(),
shouldBindVisibleOverlayToMpvSubVisibility(), isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected), setMpvSubVisibility: (mpvSubVisible: boolean) => {
setMpvSubVisibility: (mpvSubVisible: boolean) => { setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible);
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible); },
}, };
});
function getSubsyncRuntimeDeps() { function getSubsyncRuntimeDeps() {
return createSubsyncRuntimeDepsService({ return createSubsyncRuntimeDepsService({
@@ -1043,59 +1060,59 @@ function startPendingMultiCopy(timeoutMs: number): void {
function handleMultiCopyDigit(count: number): void { function handleMultiCopyDigit(count: number): void {
handleMultiCopyDigitService( handleMultiCopyDigitService(
count, count,
createHandleMultiCopyDigitDepsRuntimeService({ {
subtitleTimingTracker, subtitleTimingTracker,
writeClipboardText: (text) => clipboard.writeText(text), writeClipboardText: (text) => clipboard.writeText(text),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
function copyCurrentSubtitle(): void { function copyCurrentSubtitle(): void {
copyCurrentSubtitleService( copyCurrentSubtitleService(
createCopyCurrentSubtitleDepsRuntimeService({ {
subtitleTimingTracker, subtitleTimingTracker,
writeClipboardText: (text) => clipboard.writeText(text), writeClipboardText: (text) => clipboard.writeText(text),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
async function updateLastCardFromClipboard(): Promise<void> { async function updateLastCardFromClipboard(): Promise<void> {
await updateLastCardFromClipboardService( await updateLastCardFromClipboardService(
createUpdateLastCardFromClipboardDepsRuntimeService({ {
ankiIntegration, ankiIntegration,
readClipboardText: () => clipboard.readText(), readClipboardText: () => clipboard.readText(),
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
async function triggerFieldGrouping(): Promise<void> { async function triggerFieldGrouping(): Promise<void> {
await triggerFieldGroupingService( await triggerFieldGroupingService(
createTriggerFieldGroupingDepsRuntimeService({ {
ankiIntegration, ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
async function markLastCardAsAudioCard(): Promise<void> { async function markLastCardAsAudioCard(): Promise<void> {
await markLastCardAsAudioCardService( await markLastCardAsAudioCardService(
createMarkLastCardAsAudioCardDepsRuntimeService({ {
ankiIntegration, ankiIntegration,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
async function mineSentenceCard(): Promise<void> { async function mineSentenceCard(): Promise<void> {
await mineSentenceCardService( await mineSentenceCardService(
createMineSentenceCardDepsRuntimeService({ {
ankiIntegration, ankiIntegration,
mpvClient, mpvClient,
showMpvOsd: (text) => showMpvOsd(text), showMpvOsd: (text) => showMpvOsd(text),
}), },
); );
} }
@@ -1118,7 +1135,7 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
function handleMineSentenceDigit(count: number): void { function handleMineSentenceDigit(count: number): void {
handleMineSentenceDigitService( handleMineSentenceDigitService(
count, count,
createHandleMineSentenceDigitDepsRuntimeService({ {
subtitleTimingTracker, subtitleTimingTracker,
ankiIntegration, ankiIntegration,
getCurrentSecondarySubText: () => getCurrentSecondarySubText: () =>
@@ -1127,7 +1144,7 @@ function handleMineSentenceDigit(count: number): void {
logError: (message, err) => { logError: (message, err) => {
console.error(message, err); console.error(message, err);
}, },
}), },
); );
} }
@@ -1139,12 +1156,12 @@ function registerOverlayShortcuts(): void {
} }
function getOverlayShortcutLifecycleDeps() { function getOverlayShortcutLifecycleDeps() {
return createOverlayShortcutLifecycleDepsRuntimeService({ return {
getConfiguredShortcuts: () => getConfiguredShortcuts(), getConfiguredShortcuts: () => getConfiguredShortcuts(),
getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers, getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers,
cancelPendingMultiCopy: () => cancelPendingMultiCopy(), cancelPendingMultiCopy: () => cancelPendingMultiCopy(),
cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(), cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(),
}); };
} }
function unregisterOverlayShortcuts(): void { function unregisterOverlayShortcuts(): void {
@@ -1173,8 +1190,8 @@ function refreshOverlayShortcuts(): void {
function updateVisibleOverlayVisibility(): void { function updateVisibleOverlayVisibility(): void {
updateVisibleOverlayVisibilityService( updateVisibleOverlayVisibilityService(
createVisibleOverlayVisibilityDepsRuntimeService({ createVisibleOverlayVisibilityDepsRuntimeService({
getVisibleOverlayVisible: () => visibleOverlayVisible, getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getMainWindow: () => mainWindow, getMainWindow: () => overlayManager.getMainWindow(),
getWindowTracker: () => windowTracker, getWindowTracker: () => windowTracker,
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown, getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
setTrackerNotReadyWarningShown: (shown) => { setTrackerNotReadyWarningShown: (shown) => {
@@ -1203,9 +1220,10 @@ function updateVisibleOverlayVisibility(): void {
function updateInvisibleOverlayVisibility(): void { function updateInvisibleOverlayVisibility(): void {
updateInvisibleOverlayVisibilityService( updateInvisibleOverlayVisibilityService(
createInvisibleOverlayVisibilityDepsRuntimeService({ createInvisibleOverlayVisibilityDepsRuntimeService({
getInvisibleWindow: () => invisibleWindow, getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
getVisibleOverlayVisible: () => visibleOverlayVisible, getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => invisibleOverlayVisible, getInvisibleOverlayVisible: () =>
overlayManager.getInvisibleOverlayVisible(),
getWindowTracker: () => windowTracker, getWindowTracker: () => windowTracker,
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry), updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
@@ -1217,13 +1235,17 @@ function updateInvisibleOverlayVisibility(): void {
function syncInvisibleOverlayMousePassthrough(): void { function syncInvisibleOverlayMousePassthrough(): void {
syncInvisibleOverlayMousePassthroughService({ syncInvisibleOverlayMousePassthroughService({
hasInvisibleWindow: () => Boolean(invisibleWindow && !invisibleWindow.isDestroyed()), hasInvisibleWindow: () => {
const invisibleWindow = overlayManager.getInvisibleWindow();
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
},
setIgnoreMouseEvents: (ignore, extra) => { setIgnoreMouseEvents: (ignore, extra) => {
const invisibleWindow = overlayManager.getInvisibleWindow();
if (!invisibleWindow || invisibleWindow.isDestroyed()) return; if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
invisibleWindow.setIgnoreMouseEvents(ignore, extra); invisibleWindow.setIgnoreMouseEvents(ignore, extra);
}, },
visibleOverlayVisible, visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
invisibleOverlayVisible, invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
}); });
} }
@@ -1285,10 +1307,11 @@ const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDepsRuntimeService({
registerIpcHandlersService( registerIpcHandlersService(
createIpcDepsRuntimeService({ createIpcDepsRuntimeService({
getInvisibleWindow: () => invisibleWindow, getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
getMainWindow: () => mainWindow, getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisibility: () => visibleOverlayVisible, getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisibility: () => invisibleOverlayVisible, getInvisibleOverlayVisibility: () =>
overlayManager.getInvisibleOverlayVisible(),
onOverlayModalClosed: (modal) => onOverlayModalClosed: (modal) =>
handleOverlayModalClosed(modal as OverlayHostedModal), handleOverlayModalClosed(modal as OverlayHostedModal),
openYomitanSettings: () => openYomitanSettings(), openYomitanSettings: () => openYomitanSettings(),
@@ -1316,7 +1339,7 @@ registerIpcHandlersService(
); );
registerAnkiJimakuIpcRuntimeService( registerAnkiJimakuIpcRuntimeService(
createAnkiJimakuIpcDepsRuntimeService({ {
patchAnkiConnectEnabled: (enabled) => { patchAnkiConnectEnabled: (enabled) => {
configService.patchRawConfig({ ankiConnect: { enabled } }); configService.patchRawConfig({ ankiConnect: { enabled } });
}, },
@@ -1344,5 +1367,5 @@ registerAnkiJimakuIpcRuntimeService(
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath), isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
downloadToFile: (url, destPath, headers) => downloadToFile: (url, destPath, headers) =>
downloadToFile(url, destPath, headers), downloadToFile(url, destPath, headers),
}), },
); );