mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor runtime deps wiring and docs/config updates
This commit is contained in:
6
Makefile
6
Makefile
@@ -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
|
||||
THEME_FILE := subminer.rasi
|
||||
@@ -49,7 +49,7 @@ help:
|
||||
" build-macos Build macOS DMG/ZIP (signed if configured)" \
|
||||
" build-macos-unsigned Build macOS DMG/ZIP without signing/notarization" \
|
||||
" docs-dev Run VitePress docs dev server" \
|
||||
" docs-build Build VitePress static docs" \
|
||||
" docs Build VitePress static docs" \
|
||||
" docs-preview Preview built VitePress docs" \
|
||||
" install-linux Install Linux 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
|
||||
@pnpm run docs:dev
|
||||
|
||||
docs-build: ensure-pnpm
|
||||
docs: ensure-pnpm
|
||||
@pnpm run docs:build
|
||||
|
||||
docs-preview: ensure-pnpm
|
||||
|
||||
@@ -53,7 +53,7 @@ For macOS app bundle / signing / permissions details, use `docs/installation.md`
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
@@ -100,6 +100,7 @@ Detailed guides live in [`docs/`](docs/README.md):
|
||||
- [Usage](docs/usage.md)
|
||||
- [Configuration](docs/configuration.md)
|
||||
- [Development](docs/development.md)
|
||||
- [Architecture](docs/architecture.md) (includes `OverlayManager` state ownership and deps wiring rules)
|
||||
|
||||
### Third-Party Components
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* SubMiner Example Configuration File
|
||||
*
|
||||
* 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,
|
||||
"toggleSecondarySub": "CommandOrControl+Shift+V",
|
||||
"markAudioCard": "CommandOrControl+Shift+A",
|
||||
"openRuntimeOptions": "CommandOrControl+Shift+O"
|
||||
"openRuntimeOptions": "CommandOrControl+Shift+O",
|
||||
"openJimaku": "Ctrl+Alt+J"
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -28,10 +28,10 @@ pnpm run docs:build
|
||||
- Full config file reference and option details
|
||||
- [Development](/development)
|
||||
- Contributor notes
|
||||
- Architecture migration overview
|
||||
- Architecture and extension rules
|
||||
- Environment variables
|
||||
- License and acknowledgments
|
||||
- [Architecture](/architecture)
|
||||
- Composability migration status
|
||||
- Core runtime structure
|
||||
- Service-oriented runtime structure
|
||||
- Composition and lifecycle model
|
||||
- Extension design rules
|
||||
|
||||
@@ -1,51 +1,75 @@
|
||||
# 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
|
||||
|
||||
- `src/main.ts`: bootstrap/composition root plus remaining legacy orchestration.
|
||||
- `src/core/`: shared runtime primitives:
|
||||
- `app-orchestrator.ts`: lifecycle wiring for ready/activate/quit hooks.
|
||||
- `action-bus.ts`: typed action dispatch path.
|
||||
- `actions.ts`: canonical app action types.
|
||||
- `module.ts` / `module-registry.ts`: module lifecycle contract.
|
||||
- `services/`: extracted runtime services (IPC, shortcuts, subtitle, overlay, MPV command routing).
|
||||
- `src/ipc/`: shared IPC contract + wrappers used by main, preload, and renderer.
|
||||
- `src/subtitle/`: staged subtitle pipeline (`normalize` -> `tokenize` -> `merge`).
|
||||
- `src/modules/`: feature modules:
|
||||
- `runtime-options`
|
||||
- `jimaku`
|
||||
- `subsync`
|
||||
- `anki`
|
||||
- `texthooker`
|
||||
- provider registries:
|
||||
- `src/window-trackers/index.ts`
|
||||
- `src/tokenizers/index.ts`
|
||||
- `src/token-mergers/index.ts`
|
||||
- `src/translators/index.ts`
|
||||
- `src/subsync/engines.ts`
|
||||
- `src/main.ts`
|
||||
- Composition root for lifecycle wiring and non-overlay runtime state.
|
||||
- Owns long-lived process state for trackers, runtime flags, and client instances.
|
||||
- Delegates behavior to services.
|
||||
- `src/core/services/overlay-manager-service.ts`
|
||||
- Owns overlay/window state (`mainWindow`, `invisibleWindow`, visible/invisible overlay flags).
|
||||
- Provides a narrow state API used by `main.ts` and overlay services.
|
||||
- `src/core/services/*`
|
||||
- Stateless or narrowly stateful units for a specific responsibility.
|
||||
- Examples: startup bootstrap, app lifecycle hooks, CLI command handling, IPC registration, overlay visibility, MPV IPC behavior, shortcut registration, subtitle websocket, jimaku/subsync helpers.
|
||||
- `src/core/utils/*`
|
||||
- Pure helpers and coercion/config utilities.
|
||||
- `src/cli/*`
|
||||
- CLI parsing and help output.
|
||||
- `src/config/*`
|
||||
- Config schema/definitions, defaults, validation, and template generation.
|
||||
- `src/window-trackers/*`
|
||||
- Backend-specific tracker implementations plus selection index.
|
||||
- `src/jimaku/*`, `src/subsync/*`
|
||||
- Domain-specific integration helpers.
|
||||
|
||||
## Migration Status
|
||||
## Composition Pattern
|
||||
|
||||
- Completed:
|
||||
- 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.
|
||||
Most runtime code follows a dependency-injection pattern:
|
||||
|
||||
## 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:
|
||||
- a module (`src/modules/*`) and/or
|
||||
- a provider registration (`src/*/index.ts` registries),
|
||||
instead of editing unrelated branches in `src/main.ts`.
|
||||
- New command triggers should dispatch `AppAction` and reuse existing action handlers.
|
||||
- New IPC channels should be added only via `src/ipc/contract.ts`.
|
||||
This keeps side effects explicit and makes behavior easy to unit-test with fakes.
|
||||
|
||||
## Lifecycle Model
|
||||
|
||||
- Startup:
|
||||
- `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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@ To add or change a config option, update `src/config/definitions.ts` first. Defa
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ features:
|
||||
details: Build, test, and package SubMiner with the development notes in this docs set.
|
||||
---
|
||||
|
||||
## Documentation Sections
|
||||
<!-- ## Documentation Sections
|
||||
|
||||
- [Installation](/installation)
|
||||
- [Usage](/usage)
|
||||
- [Configuration](/configuration)
|
||||
- [Development](/development)
|
||||
- [Architecture](/architecture)
|
||||
- [Architecture](/architecture) -->
|
||||
|
||||
@@ -127,7 +127,7 @@ mpv --input-ipc-server=/tmp/subminer-socket video.mkv
|
||||
|
||||
**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:**
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* SubMiner Example Configuration File
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
{
|
||||
|
||||
|
||||
@@ -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. |
|
||||
| **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.
|
||||
|
||||
`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).
|
||||
- `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.
|
||||
- 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
|
||||
|
||||
@@ -117,12 +115,6 @@ Notes:
|
||||
|
||||
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
|
||||
|
||||
1. MPV runs with an IPC socket at `/tmp/subminer-socket`
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"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:gate5": "bash scripts/check-main-lines.sh 1500",
|
||||
"docs:dev": "vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs --host 0.0.0.0 --port 4173 --strictPort",
|
||||
"docs:dev": "VITE_EXTRA_EXTENSIONS=jsonc vitepress dev docs --host 0.0.0.0 --port 5173 --strictPort",
|
||||
"docs:build": "VITE_EXTRA_EXTENSIONS=jsonc vitepress build docs",
|
||||
"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: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",
|
||||
"generate:config-example": "pnpm run build && node dist/generate-config-example.js",
|
||||
"start": "pnpm run build && electron . --start",
|
||||
|
||||
@@ -55,7 +55,7 @@ export function generateConfigTemplate(config: ResolvedConfig = deepCloneConfig(
|
||||
lines.push(" * SubMiner Example Configuration File");
|
||||
lines.push(" *");
|
||||
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("{");
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -73,15 +73,12 @@ export {
|
||||
getOverlayWindowsRuntimeService,
|
||||
setOverlayDebugVisualizationEnabledRuntimeService,
|
||||
} from "./overlay-broadcast-runtime-service";
|
||||
export { createMpvIpcClientDepsRuntimeService } from "./mpv-client-deps-runtime-service";
|
||||
export { createAppLifecycleDepsRuntimeService } from "./app-lifecycle-deps-runtime-service";
|
||||
export { createCliCommandDepsRuntimeService } from "./cli-command-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 { createSubsyncRuntimeDepsService } from "./subsync-deps-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 { createRuntimeOptionsIpcDepsRuntimeService } from "./runtime-options-ipc-deps-runtime-service";
|
||||
export { createTokenizerDepsRuntimeService } from "./tokenizer-deps-runtime-service";
|
||||
@@ -90,26 +87,8 @@ export {
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
} from "./overlay-runtime-deps-service";
|
||||
export {
|
||||
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";
|
||||
} from "./overlay-deps-runtime-service";
|
||||
export { runOverlayShortcutLocalFallbackRuntimeService } from "./shortcut-ui-deps-runtime-service";
|
||||
export { createStartupLifecycleHooksRuntimeService } from "./startup-lifecycle-hooks-runtime-service";
|
||||
export { createRuntimeOptionsManagerRuntimeService } from "./runtime-options-manager-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 { updateInvisibleOverlayVisibilityService, updateVisibleOverlayVisibilityService } from "./overlay-visibility-service";
|
||||
export { registerAnkiJimakuIpcRuntimeService } from "./anki-jimaku-runtime-service";
|
||||
export { createOverlayManagerService } from "./overlay-manager-service";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
} from "./overlay-runtime-deps-service";
|
||||
} from "./overlay-deps-runtime-service";
|
||||
|
||||
test("createOverlayWindowRuntimeDepsService maps runtime state providers", () => {
|
||||
let visible = true;
|
||||
42
src/core/services/overlay-manager-service.test.ts
Normal file
42
src/core/services/overlay-manager-service.test.ts
Normal 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);
|
||||
});
|
||||
49
src/core/services/overlay-manager-service.ts
Normal file
49
src/core/services/overlay-manager-service.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,35 +1,11 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createGlobalShortcutRegistrationDepsRuntimeService,
|
||||
createSecondarySubtitleCycleDepsRuntimeService,
|
||||
createYomitanSettingsWindowDepsRuntimeService,
|
||||
runOverlayShortcutLocalFallbackRuntimeService,
|
||||
} from "./shortcut-ui-runtime-deps-service";
|
||||
} from "./shortcut-ui-deps-runtime-service";
|
||||
|
||||
function makeOptions() {
|
||||
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: () => ({
|
||||
toggleVisibleOverlayGlobal: 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", () => {
|
||||
const options = {
|
||||
...makeOptions(),
|
||||
24
src/core/services/shortcut-ui-deps-runtime-service.ts
Normal file
24
src/core/services/shortcut-ui-deps-runtime-service.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
AppShutdownRuntimeDeps,
|
||||
runAppShutdownRuntimeService,
|
||||
} from "./app-shutdown-runtime-service";
|
||||
import {
|
||||
createStartupAppReadyDepsRuntimeService,
|
||||
createStartupAppShutdownDepsRuntimeService,
|
||||
} from "./startup-lifecycle-runtime-deps-service";
|
||||
|
||||
type StartupLifecycleHookDeps = Pick<
|
||||
AppLifecycleDepsRuntimeOptions,
|
||||
@@ -29,14 +25,10 @@ export function createStartupLifecycleHooksRuntimeService(
|
||||
): StartupLifecycleHookDeps {
|
||||
return {
|
||||
onReady: async () => {
|
||||
await runAppReadyRuntimeService(
|
||||
createStartupAppReadyDepsRuntimeService(options.appReadyDeps),
|
||||
);
|
||||
await runAppReadyRuntimeService(options.appReadyDeps);
|
||||
},
|
||||
onWillQuitCleanup: () => {
|
||||
runAppShutdownRuntimeService(
|
||||
createStartupAppShutdownDepsRuntimeService(options.appShutdownDeps),
|
||||
);
|
||||
runAppShutdownRuntimeService(options.appShutdownDeps);
|
||||
},
|
||||
shouldRestoreWindowsOnActivate: options.shouldRestoreWindowsOnActivate,
|
||||
restoreWindowsOnActivate: options.restoreWindowsOnActivate,
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
289
src/main.ts
289
src/main.ts
@@ -22,7 +22,6 @@ import {
|
||||
clipboard,
|
||||
shell,
|
||||
protocol,
|
||||
screen,
|
||||
Extension,
|
||||
} from "electron";
|
||||
|
||||
@@ -40,18 +39,12 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
import * as path from "path";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import * as crypto from "crypto";
|
||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||
import { BaseWindowTracker } from "./window-trackers";
|
||||
import type {
|
||||
JimakuApiResponse,
|
||||
JimakuDownloadResult,
|
||||
JimakuMediaInfo,
|
||||
JimakuConfig,
|
||||
JimakuLanguagePreference,
|
||||
SubtitleData,
|
||||
SubtitlePosition,
|
||||
@@ -78,16 +71,12 @@ import {
|
||||
getSubsyncConfig,
|
||||
} from "./subsync/utils";
|
||||
import {
|
||||
hasExplicitCommand,
|
||||
parseArgs,
|
||||
shouldStartApp,
|
||||
} from "./cli/args";
|
||||
import type { CliArgs, CliCommandSource } from "./cli/args";
|
||||
import { printHelp } from "./cli/help";
|
||||
import {
|
||||
asBoolean,
|
||||
asFiniteNumber,
|
||||
asString,
|
||||
enforceUnsupportedWaylandMode,
|
||||
forceX11Backend,
|
||||
generateDefaultConfigFile,
|
||||
@@ -104,48 +93,33 @@ import {
|
||||
broadcastRuntimeOptionsChangedRuntimeService,
|
||||
broadcastToOverlayWindowsRuntimeService,
|
||||
copyCurrentSubtitleService,
|
||||
createAnkiJimakuIpcDepsRuntimeService,
|
||||
createAppLifecycleDepsRuntimeService,
|
||||
createAppLoggingRuntimeService,
|
||||
createCliCommandDepsRuntimeService,
|
||||
createCopyCurrentSubtitleDepsRuntimeService,
|
||||
createOverlayManagerService,
|
||||
createFieldGroupingOverlayRuntimeService,
|
||||
createGlobalShortcutRegistrationDepsRuntimeService,
|
||||
createHandleMineSentenceDigitDepsRuntimeService,
|
||||
createHandleMultiCopyDigitDepsRuntimeService,
|
||||
createInitializeOverlayRuntimeDepsService,
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService,
|
||||
createIpcDepsRuntimeService,
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService,
|
||||
createMecabTokenizerAndCheckRuntimeService,
|
||||
createMineSentenceCardDepsRuntimeService,
|
||||
createMpvCommandIpcDepsRuntimeService,
|
||||
createMpvIpcClientDepsRuntimeService,
|
||||
createNumericShortcutRuntimeService,
|
||||
createOverlayShortcutLifecycleDepsRuntimeService,
|
||||
createOverlayShortcutRuntimeDepsService,
|
||||
createOverlayShortcutRuntimeHandlers,
|
||||
createOverlayVisibilityFacadeDepsRuntimeService,
|
||||
createOverlayWindowRuntimeDepsService,
|
||||
createOverlayWindowService,
|
||||
createRuntimeOptionsIpcDepsRuntimeService,
|
||||
createRuntimeOptionsManagerRuntimeService,
|
||||
createSecondarySubtitleCycleDepsRuntimeService,
|
||||
createStartupLifecycleHooksRuntimeService,
|
||||
createSubsyncRuntimeDepsService,
|
||||
createSubtitleTimingTrackerRuntimeService,
|
||||
createTokenizerDepsRuntimeService,
|
||||
createTriggerFieldGroupingDepsRuntimeService,
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService,
|
||||
createVisibleOverlayVisibilityDepsRuntimeService,
|
||||
createYomitanSettingsWindowDepsRuntimeService,
|
||||
cycleSecondarySubModeService,
|
||||
enforceOverlayLayerOrderService,
|
||||
ensureOverlayWindowLevelService,
|
||||
getInitialInvisibleOverlayVisibilityService,
|
||||
getJimakuLanguagePreferenceService,
|
||||
getJimakuMaxEntryResultsService,
|
||||
getOverlayWindowsRuntimeService,
|
||||
handleCliCommandService,
|
||||
handleMineSentenceDigitService,
|
||||
handleMpvCommandFromIpcService,
|
||||
@@ -154,7 +128,6 @@ import {
|
||||
hasMpvWebsocketPlugin,
|
||||
initializeOverlayRuntimeService,
|
||||
isAutoUpdateEnabledRuntimeService,
|
||||
isGlobalShortcutRegisteredSafe,
|
||||
jimakuFetchJsonService,
|
||||
loadSubtitlePositionService,
|
||||
loadYomitanExtensionService,
|
||||
@@ -211,7 +184,41 @@ if (process.platform === "linux") {
|
||||
}
|
||||
|
||||
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 configService = new ConfigService(CONFIG_DIR);
|
||||
const isDev =
|
||||
@@ -239,8 +246,6 @@ process.on("SIGTERM", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let invisibleWindow: BrowserWindow | null = null;
|
||||
let yomitanExt: Extension | null = null;
|
||||
let yomitanSettingsWindow: 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 currentSubText = "";
|
||||
let currentSubAssText = "";
|
||||
let visibleOverlayVisible = false;
|
||||
let invisibleOverlayVisible = false;
|
||||
let windowTracker: BaseWindowTracker | null = null;
|
||||
let subtitlePosition: SubtitlePosition | null = null;
|
||||
let currentMediaPath: string | null = null;
|
||||
@@ -292,12 +295,13 @@ let fieldGroupingResolver: ((choice: KikuFieldGroupingChoice) => void) | null =
|
||||
let runtimeOptionsManager: RuntimeOptionsManager | null = null;
|
||||
let trackerNotReadyWarningShown = false;
|
||||
let overlayDebugVisualizationEnabled = false;
|
||||
const overlayManager = createOverlayManagerService();
|
||||
type OverlayHostedModal = "runtime-options" | "subsync";
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntimeService<OverlayHostedModal>({
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||
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 getOverlayWindows(): BrowserWindow[] {
|
||||
return getOverlayWindowsRuntimeService({ mainWindow, invisibleWindow });
|
||||
return overlayManager.getOverlayWindows();
|
||||
}
|
||||
|
||||
function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||
@@ -492,13 +496,14 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
createMpvClient: () => {
|
||||
mpvClient = new MpvIpcClient(
|
||||
mpvSocketPath,
|
||||
createMpvIpcClientDepsRuntimeService({
|
||||
{
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
autoStartOverlay,
|
||||
setOverlayVisible: (visible) => setOverlayVisible(visible),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
isVisibleOverlayVisible: () =>
|
||||
overlayManager.getVisibleOverlayVisible(),
|
||||
getReconnectTimer: () => reconnectTimer,
|
||||
setReconnectTimer: (timer) => {
|
||||
reconnectTimer = timer;
|
||||
@@ -532,11 +537,12 @@ const startupState = runStartupBootstrapRuntimeService({
|
||||
showMpvOsd: (text) => {
|
||||
showMpvOsd(text);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
},
|
||||
reloadConfig: () => {
|
||||
configService.reloadConfig();
|
||||
appLogger.logInfo(`Using config file: ${configService.getConfigPath()}`);
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getConfigWarnings: () => configService.getWarnings(),
|
||||
@@ -705,7 +711,7 @@ function handleCliCommand(
|
||||
},
|
||||
app: {
|
||||
stop: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(mainWindow),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
},
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn, delayMs) => setTimeout(fn, delayMs),
|
||||
@@ -773,10 +779,10 @@ function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
||||
|
||||
function enforceOverlayLayerOrder(): void {
|
||||
enforceOverlayLayerOrderService({
|
||||
visibleOverlayVisible,
|
||||
invisibleOverlayVisible,
|
||||
mainWindow,
|
||||
invisibleWindow,
|
||||
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
||||
mainWindow: overlayManager.getMainWindow(),
|
||||
invisibleWindow: overlayManager.getInvisibleWindow(),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
});
|
||||
}
|
||||
@@ -810,23 +816,31 @@ function createOverlayWindow(kind: "visible" | "invisible"): BrowserWindow {
|
||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) =>
|
||||
setOverlayDebugVisualizationEnabled(enabled),
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
if (windowKind === "visible") {
|
||||
mainWindow = null;
|
||||
overlayManager.setMainWindow(null);
|
||||
} else {
|
||||
invisibleWindow = null;
|
||||
overlayManager.setInvisibleWindow(null);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function createMainWindow(): BrowserWindow { mainWindow = createOverlayWindow("visible"); return mainWindow; }
|
||||
function createInvisibleWindow(): BrowserWindow { invisibleWindow = createOverlayWindow("invisible"); return invisibleWindow; }
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow("visible");
|
||||
overlayManager.setMainWindow(window);
|
||||
return window;
|
||||
}
|
||||
function createInvisibleWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow("invisible");
|
||||
overlayManager.setInvisibleWindow(window);
|
||||
return window;
|
||||
}
|
||||
|
||||
function initializeOverlayRuntime(): void {
|
||||
if (overlayRuntimeInitialized) {
|
||||
@@ -849,8 +863,9 @@ function initializeOverlayRuntime(): void {
|
||||
updateOverlayBounds: (geometry) => {
|
||||
updateOverlayBounds(geometry);
|
||||
},
|
||||
isVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
isInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => {
|
||||
updateVisibleOverlayVisibility();
|
||||
},
|
||||
@@ -875,35 +890,12 @@ function initializeOverlayRuntime(): void {
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
}),
|
||||
);
|
||||
invisibleOverlayVisible = result.invisibleOverlayVisible;
|
||||
overlayManager.setInvisibleOverlayVisible(result.invisibleOverlayVisible);
|
||||
overlayRuntimeInitialized = true;
|
||||
}
|
||||
|
||||
function getShortcutUiRuntimeDeps() {
|
||||
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(),
|
||||
getOverlayShortcutFallbackHandlers: () =>
|
||||
getOverlayShortcutRuntimeHandlers().fallbackHandlers,
|
||||
@@ -913,12 +905,25 @@ function getShortcutUiRuntimeDeps() {
|
||||
|
||||
function openYomitanSettings(): void {
|
||||
openYomitanSettingsWindow(
|
||||
createYomitanSettingsWindowDepsRuntimeService(getShortcutUiRuntimeDeps()),
|
||||
{
|
||||
yomitanExt,
|
||||
getExistingWindow: () => yomitanSettingsWindow,
|
||||
setWindow: (window: BrowserWindow | null) => {
|
||||
yomitanSettingsWindow = window;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
function registerGlobalShortcuts(): void {
|
||||
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() {
|
||||
return createOverlayShortcutRuntimeHandlers(
|
||||
createOverlayShortcutRuntimeDepsService({
|
||||
{
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
openRuntimeOptions: () => {
|
||||
openRuntimeOptionsPalette();
|
||||
@@ -949,7 +954,7 @@ function getOverlayShortcutRuntimeHandlers() {
|
||||
mineSentenceMultiple: (timeoutMs) => {
|
||||
startPendingMineSentenceMultiple(timeoutMs);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -962,7 +967,20 @@ function tryHandleOverlayShortcutLocalFallback(input: Electron.Input): boolean {
|
||||
|
||||
function cycleSecondarySubMode(): void {
|
||||
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 mineSentenceSession = numericShortcutRuntime.createSession();
|
||||
const overlayVisibilityFacadeDeps =
|
||||
createOverlayVisibilityFacadeDepsRuntimeService({
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
setVisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
visibleOverlayVisible = nextVisible;
|
||||
},
|
||||
setInvisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
invisibleOverlayVisible = nextVisible;
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible: boolean) => {
|
||||
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible);
|
||||
},
|
||||
});
|
||||
const overlayVisibilityFacadeDeps = {
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
setInvisibleOverlayVisibleState: (nextVisible: boolean) => {
|
||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(mpvClient && mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible: boolean) => {
|
||||
setMpvSubVisibilityRuntimeService(mpvClient, mpvSubVisible);
|
||||
},
|
||||
};
|
||||
|
||||
function getSubsyncRuntimeDeps() {
|
||||
return createSubsyncRuntimeDepsService({
|
||||
@@ -1043,59 +1060,59 @@ function startPendingMultiCopy(timeoutMs: number): void {
|
||||
function handleMultiCopyDigit(count: number): void {
|
||||
handleMultiCopyDigitService(
|
||||
count,
|
||||
createHandleMultiCopyDigitDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText: (text) => clipboard.writeText(text),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function copyCurrentSubtitle(): void {
|
||||
copyCurrentSubtitleService(
|
||||
createCopyCurrentSubtitleDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
writeClipboardText: (text) => clipboard.writeText(text),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function updateLastCardFromClipboard(): Promise<void> {
|
||||
await updateLastCardFromClipboardService(
|
||||
createUpdateLastCardFromClipboardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
readClipboardText: () => clipboard.readText(),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function triggerFieldGrouping(): Promise<void> {
|
||||
await triggerFieldGroupingService(
|
||||
createTriggerFieldGroupingDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function markLastCardAsAudioCard(): Promise<void> {
|
||||
await markLastCardAsAudioCardService(
|
||||
createMarkLastCardAsAudioCardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function mineSentenceCard(): Promise<void> {
|
||||
await mineSentenceCardService(
|
||||
createMineSentenceCardDepsRuntimeService({
|
||||
{
|
||||
ankiIntegration,
|
||||
mpvClient,
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1118,7 +1135,7 @@ function startPendingMineSentenceMultiple(timeoutMs: number): void {
|
||||
function handleMineSentenceDigit(count: number): void {
|
||||
handleMineSentenceDigitService(
|
||||
count,
|
||||
createHandleMineSentenceDigitDepsRuntimeService({
|
||||
{
|
||||
subtitleTimingTracker,
|
||||
ankiIntegration,
|
||||
getCurrentSecondarySubText: () =>
|
||||
@@ -1127,7 +1144,7 @@ function handleMineSentenceDigit(count: number): void {
|
||||
logError: (message, err) => {
|
||||
console.error(message, err);
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1139,12 +1156,12 @@ function registerOverlayShortcuts(): void {
|
||||
}
|
||||
|
||||
function getOverlayShortcutLifecycleDeps() {
|
||||
return createOverlayShortcutLifecycleDepsRuntimeService({
|
||||
return {
|
||||
getConfiguredShortcuts: () => getConfiguredShortcuts(),
|
||||
getOverlayHandlers: () => getOverlayShortcutRuntimeHandlers().overlayHandlers,
|
||||
cancelPendingMultiCopy: () => cancelPendingMultiCopy(),
|
||||
cancelPendingMineSentenceMultiple: () => cancelPendingMineSentenceMultiple(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function unregisterOverlayShortcuts(): void {
|
||||
@@ -1173,8 +1190,8 @@ function refreshOverlayShortcuts(): void {
|
||||
function updateVisibleOverlayVisibility(): void {
|
||||
updateVisibleOverlayVisibilityService(
|
||||
createVisibleOverlayVisibilityDepsRuntimeService({
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getWindowTracker: () => windowTracker,
|
||||
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
|
||||
setTrackerNotReadyWarningShown: (shown) => {
|
||||
@@ -1203,9 +1220,10 @@ function updateVisibleOverlayVisibility(): void {
|
||||
function updateInvisibleOverlayVisibility(): void {
|
||||
updateInvisibleOverlayVisibilityService(
|
||||
createInvisibleOverlayVisibilityDepsRuntimeService({
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getVisibleOverlayVisible: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
getWindowTracker: () => windowTracker,
|
||||
updateOverlayBounds: (geometry) => updateOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
@@ -1217,13 +1235,17 @@ function updateInvisibleOverlayVisibility(): void {
|
||||
|
||||
function syncInvisibleOverlayMousePassthrough(): void {
|
||||
syncInvisibleOverlayMousePassthroughService({
|
||||
hasInvisibleWindow: () => Boolean(invisibleWindow && !invisibleWindow.isDestroyed()),
|
||||
hasInvisibleWindow: () => {
|
||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
||||
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
|
||||
},
|
||||
setIgnoreMouseEvents: (ignore, extra) => {
|
||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||
invisibleWindow.setIgnoreMouseEvents(ignore, extra);
|
||||
},
|
||||
visibleOverlayVisible,
|
||||
invisibleOverlayVisible,
|
||||
visibleOverlayVisible: overlayManager.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: overlayManager.getInvisibleOverlayVisible(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1285,10 +1307,11 @@ const runtimeOptionsIpcDeps = createRuntimeOptionsIpcDepsRuntimeService({
|
||||
|
||||
registerIpcHandlersService(
|
||||
createIpcDepsRuntimeService({
|
||||
getInvisibleWindow: () => invisibleWindow,
|
||||
getMainWindow: () => mainWindow,
|
||||
getVisibleOverlayVisibility: () => visibleOverlayVisible,
|
||||
getInvisibleOverlayVisibility: () => invisibleOverlayVisible,
|
||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
||||
getMainWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisibility: () =>
|
||||
overlayManager.getInvisibleOverlayVisible(),
|
||||
onOverlayModalClosed: (modal) =>
|
||||
handleOverlayModalClosed(modal as OverlayHostedModal),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
@@ -1316,7 +1339,7 @@ registerIpcHandlersService(
|
||||
);
|
||||
|
||||
registerAnkiJimakuIpcRuntimeService(
|
||||
createAnkiJimakuIpcDepsRuntimeService({
|
||||
{
|
||||
patchAnkiConnectEnabled: (enabled) => {
|
||||
configService.patchRawConfig({ ankiConnect: { enabled } });
|
||||
},
|
||||
@@ -1344,5 +1367,5 @@ registerAnkiJimakuIpcRuntimeService(
|
||||
isRemoteMediaPath: (mediaPath) => isRemoteMediaPath(mediaPath),
|
||||
downloadToFile: (url, destPath, headers) =>
|
||||
downloadToFile(url, destPath, headers),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user