mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 18:22:41 -08:00
feat: bind overlay state to secondary subtitle mpv visibility
This commit is contained in:
@@ -13,11 +13,11 @@
|
|||||||
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
"auto_start_overlay": false, // When overlay connects to mpv, automatically show overlay and hide mpv subtitles. Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Visible Overlay Subtitle Binding
|
// Visible Overlay Subtitle Binding (Legacy)
|
||||||
// Control whether visible overlay toggles also toggle MPV subtitle visibility.
|
// Backward-compatible key. Runtime ignores this value.
|
||||||
// When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged.
|
// MPV subtitles are automatically hidden whenever any overlay subtitle mode is visible.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"bind_visible_overlay_to_mpv_sub_visibility": true, // Link visible overlay toggles to MPV subtitle visibility (primary and secondary). Values: true | false
|
"bind_visible_overlay_to_mpv_sub_visibility": true, // Legacy compatibility key (runtime no-op). Values: true | false
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Texthooker Server
|
// Texthooker Server
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
"toggleVisibleOverlayGlobal": "Alt+Shift+O", // Toggle visible overlay global setting.
|
||||||
"toggleInvisibleOverlayGlobal": "Alt+Shift+I", // Toggle invisible overlay global setting.
|
|
||||||
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
"copySubtitle": "CommandOrControl+C", // Copy subtitle setting.
|
||||||
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
"copySubtitleMultiple": "CommandOrControl+Shift+C", // Copy subtitle multiple setting.
|
||||||
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
"updateLastCardFromClipboard": "CommandOrControl+V", // Update last card from clipboard setting.
|
||||||
@@ -68,16 +67,6 @@
|
|||||||
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
"openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
|
||||||
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// Invisible Overlay
|
|
||||||
// Startup behavior for the invisible interactive subtitle mining layer.
|
|
||||||
// Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel.
|
|
||||||
// This edit-mode shortcut is fixed and is not currently configurable.
|
|
||||||
// ==========================================
|
|
||||||
"invisibleOverlay": {
|
|
||||||
"startupVisibility": "platform-default" // Startup visibility setting.
|
|
||||||
}, // Startup behavior for the invisible interactive subtitle mining layer.
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Extra keybindings that are merged with built-in defaults.
|
// Extra keybindings that are merged with built-in defaults.
|
||||||
@@ -123,9 +112,11 @@
|
|||||||
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
|
// Additional CSS declarations are also allowed and applied directly to subtitle roots/containers (for example: lineHeight, textShadow, -webkit-text-stroke, backdropFilter).
|
||||||
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
"enableJlpt": false, // Enable JLPT vocabulary level underlines. When disabled, JLPT tagging lookup and underlines are skipped. Values: true | false
|
||||||
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
"preserveLineBreaks": false, // Preserve line breaks in visible overlay subtitle rendering. When false, line breaks are flattened to spaces for a single-line flow. Values: true | false
|
||||||
"hoverTokenColor": "#c6a0f6", // Hex color used for hovered subtitle token highlight in mpv.
|
"hoverTokenColor": "#f4dbd6", // Hex color used for hovered subtitle token highlight in the overlay.
|
||||||
|
"hoverTokenBackgroundColor": "#363a4fd6", // CSS color used for hovered subtitle token background highlight in the overlay.
|
||||||
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting.
|
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting.
|
||||||
"fontSize": 35, // Font size setting.
|
"fontSize": 35, // Font size setting.
|
||||||
"fontColor": "#cad3f5", // Font color setting.
|
"fontColor": "#cad3f5", // Font color setting.
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ src/
|
|||||||
|
|
||||||
### Service Layer (`src/core/services/`)
|
### Service Layer (`src/core/services/`)
|
||||||
|
|
||||||
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-window-geometry.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`, `overlay-drop.ts`
|
- **Overlay/window runtime:** `overlay-manager.ts`, `overlay-window.ts`, `overlay-visibility.ts`, `overlay-bridge.ts`, `overlay-runtime-init.ts`, `overlay-content-measurement.ts`, `overlay-drop.ts`
|
||||||
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
- **Shortcuts/input:** `shortcut.ts`, `overlay-shortcut.ts`, `overlay-shortcut-handler.ts`, `shortcut-fallback.ts`, `numeric-shortcut.ts`
|
||||||
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
- **MPV runtime:** `mpv.ts`, `mpv-transport.ts`, `mpv-protocol.ts`, `mpv-properties.ts`, `mpv-render-metrics.ts`
|
||||||
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
- **Mining + Anki/Jimaku runtime:** `mining.ts`, `field-grouping.ts`, `field-grouping-overlay.ts`, `anki-jimaku.ts`, `anki-jimaku-ipc.ts`
|
||||||
@@ -102,7 +102,6 @@ src/renderer/
|
|||||||
positioning.ts # Facade export for positioning controller
|
positioning.ts # Facade export for positioning controller
|
||||||
positioning/
|
positioning/
|
||||||
controller.ts # Position controller orchestration
|
controller.ts # Position controller orchestration
|
||||||
invisible-layout*.ts # Invisible layer layout computations
|
|
||||||
position-state.ts # Position state helpers
|
position-state.ts # Position state helpers
|
||||||
handlers/
|
handlers/
|
||||||
keyboard.ts # Keybindings, chord handling, modal key routing
|
keyboard.ts # Keybindings, chord handling, modal key routing
|
||||||
@@ -125,7 +124,7 @@ src/renderer/
|
|||||||
|
|
||||||
## Flow Diagram
|
## Flow Diagram
|
||||||
|
|
||||||
The main process has three layers: `main.ts` delegates to composition modules that wire together domain services. Three overlay windows (visible, invisible, secondary) run in separate Electron renderer processes, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
The main process orchestrates a single primary overlay window plus modal surfaces: `main.ts` delegates to composition modules that wire together domain services. Subtitle layers (primary + secondary bar) are rendered in the same overlay renderer process, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -162,7 +161,7 @@ flowchart LR
|
|||||||
|
|
||||||
subgraph Svc["Services — src/core/services/"]
|
subgraph Svc["Services — src/core/services/"]
|
||||||
Mpv["MPV Stack<br/>transport · protocol<br/>properties · metrics"]:::svc
|
Mpv["MPV Stack<br/>transport · protocol<br/>properties · metrics"]:::svc
|
||||||
Overlay["Overlay Manager<br/>window · geometry<br/>visibility · bridge"]:::svc
|
Overlay["Overlay Manager<br/>window · visibility · bridge"]:::svc
|
||||||
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
|
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
|
||||||
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
|
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
|
||||||
Tracking["Tracking<br/>anilist · jellyfin<br/>immersion · discord"]:::svc
|
Tracking["Tracking<br/>anilist · jellyfin<br/>immersion · discord"]:::svc
|
||||||
@@ -172,9 +171,7 @@ flowchart LR
|
|||||||
Bridge(["preload.ts<br/>Electron IPC"]):::bridge
|
Bridge(["preload.ts<br/>Electron IPC"]):::bridge
|
||||||
|
|
||||||
subgraph Rend["Renderer — src/renderer/"]
|
subgraph Rend["Renderer — src/renderer/"]
|
||||||
Visible["Visible window<br/>Yomitan lookups"]:::rend
|
Overlay["Main overlay window<br/>primary + secondary subtitles"]:::rend
|
||||||
Invisible["Invisible window<br/>mpv positioning"]:::rend
|
|
||||||
Secondary["Secondary window<br/>subtitle bar"]:::rend
|
|
||||||
UI["subtitle-render<br/>positioning<br/>handlers · modals"]:::rend
|
UI["subtitle-render<br/>positioning<br/>handlers · modals"]:::rend
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -193,10 +190,8 @@ flowchart LR
|
|||||||
DiscordExt <-->|"RPC"| Integrations
|
DiscordExt <-->|"RPC"| Integrations
|
||||||
|
|
||||||
Overlay & Mining --> Bridge
|
Overlay & Mining --> Bridge
|
||||||
Bridge --> Visible
|
Bridge --> Overlay
|
||||||
Bridge --> Invisible
|
Overlay --> UI
|
||||||
Bridge --> Secondary
|
|
||||||
Visible & Invisible & Secondary --> UI
|
|
||||||
|
|
||||||
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
|
||||||
@@ -264,9 +259,9 @@ For domains migrated to reducer-style transitions (for example AniList token/que
|
|||||||
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
- **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
|
||||||
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
- **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
|
||||||
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
|
- **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
|
||||||
- **Overlay runtime:** `initializeOverlayRuntime()` creates three overlay windows — **visible** (interactive Yomitan lookups), **invisible** (mpv-matched subtitle positioning), and **secondary** (secondary subtitle bar, top 20% via `splitOverlayGeometryForSecondaryBar`) — then registers global shortcuts and sets initial bounds from the window tracker.
|
- **Overlay runtime:** `initializeOverlayRuntime()` creates the primary overlay window (interactive Yomitan lookups and subtitle rendering) and registers global shortcuts and bounds tracking via the active window tracker.
|
||||||
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check, Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, and AniList token refresh.
|
- **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check, Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, and AniList token refresh.
|
||||||
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results broadcast to all overlay windows.
|
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results are sent to the main overlay renderer and modal surfaces.
|
||||||
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, and cleans Anki/AniList state.
|
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, and cleans Anki/AniList state.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -298,14 +293,10 @@ flowchart LR
|
|||||||
|
|
||||||
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
|
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
|
||||||
|
|
||||||
OverlayInit --> VisWin["Visible window<br/>Yomitan lookups"]:::init
|
OverlayInit --> MainWin["Main overlay window<br/>primary + secondary subtitles"]:::init
|
||||||
OverlayInit --> InvWin["Invisible window<br/>mpv positioning"]:::init
|
|
||||||
OverlayInit --> SecWin["Secondary window<br/>subtitle bar"]:::init
|
|
||||||
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
|
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
|
||||||
|
|
||||||
VisWin --> Warmups
|
MainWin --> Warmups
|
||||||
InvWin --> Warmups
|
|
||||||
SecWin --> Warmups
|
|
||||||
Shortcuts --> Warmups
|
Shortcuts --> Warmups
|
||||||
|
|
||||||
Warmups["Background<br/>warmups"]:::phase
|
Warmups["Background<br/>warmups"]:::phase
|
||||||
@@ -330,7 +321,7 @@ flowchart LR
|
|||||||
ExtEvt["Shortcuts · config hot-reload"]:::runtime
|
ExtEvt["Shortcuts · config hot-reload"]:::runtime
|
||||||
MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
|
MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
|
||||||
Route --> Process["SubtitlePipeline<br/>normalize → tokenize → merge"]:::runtime
|
Route --> Process["SubtitlePipeline<br/>normalize → tokenize → merge"]:::runtime
|
||||||
Process --> Broadcast["Update AppState<br/>broadcast to windows"]:::runtime
|
Process --> Broadcast["Update AppState<br/>broadcast to renderer + modals"]:::runtime
|
||||||
end
|
end
|
||||||
|
|
||||||
WarmupGroup --> Loop
|
WarmupGroup --> Loop
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ features:
|
|||||||
- icon:
|
- icon:
|
||||||
src: /assets/dual-layer.svg
|
src: /assets/dual-layer.svg
|
||||||
alt: Dual layer icon
|
alt: Dual layer icon
|
||||||
title: Three-Plane Overlay Stack
|
title: Unified Overlay Stack
|
||||||
details: Secondary context plane + visible interactive layer + invisible interaction plane, each with independent behavior and startup state.
|
details: Primary interactive subtitle layer with a built-in secondary context bar, all in one overlay window.
|
||||||
- icon:
|
- icon:
|
||||||
src: /assets/highlight.svg
|
src: /assets/highlight.svg
|
||||||
alt: Highlight icon
|
alt: Highlight icon
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ SubMiner prioritizes subtitle responsiveness over heavy initialization:
|
|||||||
|
|
||||||
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
This keeps early playback snappy and avoids mpv-side sluggishness while startup work completes.
|
||||||
|
|
||||||
## The Three Overlay Planes
|
## Overlay Model
|
||||||
|
|
||||||
SubMiner uses three overlay planes, each serving a different purpose.
|
SubMiner uses one overlay window with modal surfaces.
|
||||||
|
|
||||||
### Visible Overlay
|
### Primary Subtitle Layer
|
||||||
|
|
||||||
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
The visible overlay renders subtitles as tokenized, clickable word spans. Each word is a separate element with reading and headword data attached. This plane is styled independently from mpv subtitles and supports:
|
||||||
|
|
||||||
@@ -38,31 +38,17 @@ The visible overlay renders subtitles as tokenized, clickable word spans. Each w
|
|||||||
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
- Modal dialogs for Jimaku search, field grouping, subsync, and runtime options
|
||||||
- **N+1 highlighting** — known words from your Anki deck are visually highlighted
|
- **N+1 highlighting** — known words from your Anki deck are visually highlighted
|
||||||
|
|
||||||
Toggle with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
Toggle visibility with `Alt+Shift+O` (global) or `y-t` (mpv plugin).
|
||||||
|
|
||||||
### Secondary Subtitle Plane
|
### Secondary Subtitle Bar
|
||||||
|
|
||||||
The secondary plane is a compact top-strip layer for translation and context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
|
The secondary subtitle bar is a compact top-strip region in the same overlay window for translation/context visibility while keeping primary reading flow below. It mirrors your configured secondary subtitle preference and can be independently shown or hidden.
|
||||||
|
|
||||||
It is controlled by `secondarySub` configuration and shares lifecycle with the overlay stack.
|
It is controlled by `secondarySub` configuration and shares lifecycle with the main overlay window.
|
||||||
|
|
||||||
### Invisible Overlay
|
### Modal Surfaces
|
||||||
|
|
||||||
The invisible overlay is a transparent layer aligned with mpv's own subtitle rendering. It uses mpv's subtitle metrics (font size, margins, position, scaling) to map click targets accurately.
|
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
|
||||||
|
|
||||||
This layer still supports:
|
|
||||||
|
|
||||||
- Word-level click-through lookups over the text region
|
|
||||||
- Optional manual position fine-tuning in pixel mode
|
|
||||||
- Independent toggle behavior with global shortcuts
|
|
||||||
|
|
||||||
Position edit mode is available via `Ctrl/Cmd+Shift+P`, then arrow keys / `hjkl` to nudge position; `Shift` moves faster. Save with `Enter` or `Ctrl+S`, cancel with `Esc`.
|
|
||||||
|
|
||||||
Toggle controls:
|
|
||||||
|
|
||||||
- `Alt+Shift+O` / `y-t`: visible overlay
|
|
||||||
- `Alt+Shift+I` / `y-i`: invisible overlay
|
|
||||||
- Secondary plane visibility is controlled via `secondarySub` config and matching global shortcuts.
|
|
||||||
|
|
||||||
## Looking Up Words
|
## Looking Up Words
|
||||||
|
|
||||||
@@ -73,10 +59,10 @@ Toggle controls:
|
|||||||
3. Yomitan detects the text selection and opens its popup with dictionary results.
|
3. Yomitan detects the text selection and opens its popup with dictionary results.
|
||||||
4. From the Yomitan popup, you can add the word directly to Anki.
|
4. From the Yomitan popup, you can add the word directly to Anki.
|
||||||
|
|
||||||
### On the Invisible Overlay
|
### On Overlay Subtitles
|
||||||
|
|
||||||
1. The invisible layer sits over mpv's own subtitle text.
|
1. Subtitles are rendered directly in the overlay.
|
||||||
2. Click on any word in the subtitle — SubMiner maps your click position to the underlying text.
|
2. Click on any word in the subtitle.
|
||||||
3. On macOS, word selection happens automatically on hover.
|
3. On macOS, word selection happens automatically on hover.
|
||||||
4. Yomitan popup appears for lookup and card creation.
|
4. Yomitan popup appears for lookup and card creation.
|
||||||
|
|
||||||
|
|||||||
55
docs/plans/2026-02-26-secondary-subtitles-main-overlay.md
Normal file
55
docs/plans/2026-02-26-secondary-subtitles-main-overlay.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Secondary Subtitles Main Overlay Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Ensure secondary subtitles render in the unified main overlay window and remove stale secondary-window/layer paths.
|
||||||
|
|
||||||
|
**Architecture:** Keep secondary subtitle DOM in the shared renderer tree, rely on mode classes (`secondary-sub-hidden|visible|hover`) for visibility, and remove obsolete legacy overlay-layer assumptions. Preserve modal behavior and existing subtitle rendering flow.
|
||||||
|
|
||||||
|
**Tech Stack:** TypeScript, Electron renderer CSS/DOM, Bun test runner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Regression Tests For Main Overlay Secondary Rendering
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/renderer/subtitle-render.test.ts`
|
||||||
|
- Modify: `src/renderer/error-recovery.test.ts`
|
||||||
|
|
||||||
|
**Step 1: Write failing tests**
|
||||||
|
- Assert stylesheet no longer hides secondary subtitles in `layer-visible`.
|
||||||
|
- Assert renderer platform resolution ignores legacy `secondary` overlay layer.
|
||||||
|
|
||||||
|
**Step 2: Run tests to verify failures**
|
||||||
|
|
||||||
|
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts`
|
||||||
|
Expected: FAIL on secondary subtitle hide rule + legacy secondary layer handling.
|
||||||
|
|
||||||
|
### Task 2: Remove Secondary-Window CSS/Routing Assumptions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/renderer/style.css`
|
||||||
|
- Modify: `src/renderer/utils/platform.ts`
|
||||||
|
- Modify: `src/renderer/error-recovery.ts`
|
||||||
|
- Modify: `src/types.ts`
|
||||||
|
|
||||||
|
**Step 1: Implement minimal changes**
|
||||||
|
- Remove legacy forced hide on `#secondarySubContainer`.
|
||||||
|
- Remove obsolete layer-specific secondary-subtitle CSS blocks.
|
||||||
|
- Drop legacy `secondary` overlay-layer parsing path from renderer platform resolver.
|
||||||
|
- Narrow related overlay layer type unions.
|
||||||
|
|
||||||
|
**Step 2: Run targeted tests**
|
||||||
|
|
||||||
|
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Validate Wider Related Surface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- No additional code changes required.
|
||||||
|
|
||||||
|
**Step 1: Run broader related tests**
|
||||||
|
|
||||||
|
Run: `bun test src/renderer/subtitle-render.test.ts src/renderer/error-recovery.test.ts src/main/runtime/overlay-window-runtime-handlers.test.ts src/main/runtime/overlay-window-factory.test.ts src/core/services/overlay-manager.test.ts`
|
||||||
|
Expected: Renderer tests pass; report any unrelated pre-existing failures.
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
"test:config:smoke:dist": "bun test dist/config/path-resolution.test.js",
|
||||||
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
|
||||||
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
|
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/smoke.e2e.test.ts",
|
||||||
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/positioning/invisible-layout-helpers.test.ts src/renderer/positioning/invisible-layout-metrics.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
"test:core:src": "bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/utils/shortcut-config.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/x11-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts",
|
||||||
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/positioning/invisible-layout-helpers.test.js dist/renderer/positioning/invisible-layout-metrics.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:dist": "bun test dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
|
||||||
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
|
||||||
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
|
"test:subtitle:dist": "echo \"Subtitle tests are currently not configured\"",
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
|||||||
if (isObject(src.shortcuts)) {
|
if (isObject(src.shortcuts)) {
|
||||||
const shortcutKeys = [
|
const shortcutKeys = [
|
||||||
'toggleVisibleOverlayGlobal',
|
'toggleVisibleOverlayGlobal',
|
||||||
'toggleInvisibleOverlayGlobal',
|
|
||||||
'copySubtitle',
|
'copySubtitle',
|
||||||
'copySubtitleMultiple',
|
'copySubtitleMultiple',
|
||||||
'updateLastCardFromClipboard',
|
'updateLastCardFromClipboard',
|
||||||
@@ -113,24 +112,6 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isObject(src.invisibleOverlay)) {
|
|
||||||
const startupVisibility = src.invisibleOverlay.startupVisibility;
|
|
||||||
if (
|
|
||||||
startupVisibility === 'platform-default' ||
|
|
||||||
startupVisibility === 'visible' ||
|
|
||||||
startupVisibility === 'hidden'
|
|
||||||
) {
|
|
||||||
resolved.invisibleOverlay.startupVisibility = startupVisibility;
|
|
||||||
} else if (startupVisibility !== undefined) {
|
|
||||||
warn(
|
|
||||||
'invisibleOverlay.startupVisibility',
|
|
||||||
startupVisibility,
|
|
||||||
resolved.invisibleOverlay.startupVisibility,
|
|
||||||
'Expected platform-default, visible, or hidden.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isObject(src.secondarySub)) {
|
if (isObject(src.secondarySub)) {
|
||||||
if (Array.isArray(src.secondarySub.secondarySubLanguages)) {
|
if (Array.isArray(src.secondarySub.secondarySubLanguages)) {
|
||||||
resolved.secondarySub.secondarySubLanguages = src.secondarySub.secondarySubLanguages.filter(
|
resolved.secondarySub.secondarySubLanguages = src.secondarySub.secondarySubLanguages.filter(
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export {
|
|||||||
export { createAppLifecycleDepsRuntime, startAppLifecycle } from './app-lifecycle';
|
export { createAppLifecycleDepsRuntime, startAppLifecycle } from './app-lifecycle';
|
||||||
export { cycleSecondarySubMode } from './subtitle-position';
|
export { cycleSecondarySubMode } from './subtitle-position';
|
||||||
export {
|
export {
|
||||||
getInitialInvisibleOverlayVisibility,
|
|
||||||
isAutoUpdateEnabledRuntime,
|
isAutoUpdateEnabledRuntime,
|
||||||
shouldAutoInitializeOverlayRuntimeFromConfig,
|
shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility,
|
shouldBindVisibleOverlayToMpvSubVisibility,
|
||||||
@@ -59,14 +58,12 @@ export {
|
|||||||
createOverlayWindow,
|
createOverlayWindow,
|
||||||
enforceOverlayLayerOrder,
|
enforceOverlayLayerOrder,
|
||||||
ensureOverlayWindowLevel,
|
ensureOverlayWindowLevel,
|
||||||
|
syncOverlayWindowLayer,
|
||||||
updateOverlayWindowBounds,
|
updateOverlayWindowBounds,
|
||||||
} from './overlay-window';
|
} from './overlay-window';
|
||||||
export { initializeOverlayRuntime } from './overlay-runtime-init';
|
export { initializeOverlayRuntime } from './overlay-runtime-init';
|
||||||
export {
|
export {
|
||||||
setInvisibleOverlayVisible,
|
|
||||||
setVisibleOverlayVisible,
|
setVisibleOverlayVisible,
|
||||||
syncInvisibleOverlayMousePassthrough,
|
|
||||||
updateInvisibleOverlayVisibility,
|
|
||||||
updateVisibleOverlayVisibility,
|
updateVisibleOverlayVisibility,
|
||||||
} from './overlay-visibility';
|
} from './overlay-visibility';
|
||||||
export {
|
export {
|
||||||
@@ -76,6 +73,7 @@ export {
|
|||||||
replayCurrentSubtitleRuntime,
|
replayCurrentSubtitleRuntime,
|
||||||
resolveCurrentAudioStreamIndex,
|
resolveCurrentAudioStreamIndex,
|
||||||
sendMpvCommandRuntime,
|
sendMpvCommandRuntime,
|
||||||
|
setMpvSecondarySubVisibilityRuntime,
|
||||||
setMpvSubVisibilityRuntime,
|
setMpvSubVisibilityRuntime,
|
||||||
showMpvOsdRuntime,
|
showMpvOsdRuntime,
|
||||||
} from './mpv';
|
} from './mpv';
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
|
|||||||
'sub-use-margins',
|
'sub-use-margins',
|
||||||
'pause',
|
'pause',
|
||||||
'media-title',
|
'media-title',
|
||||||
|
'secondary-sub-visibility',
|
||||||
|
'sub-visibility',
|
||||||
];
|
];
|
||||||
|
|
||||||
const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
|
const MPV_INITIAL_PROPERTY_REQUESTS: Array<MpvProtocolCommand> = [
|
||||||
|
|||||||
@@ -119,6 +119,38 @@ test('dispatchMpvProtocolMessage emits subtitle text on property change', async
|
|||||||
assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]);
|
assert.deepEqual(state.events, [{ text: '字幕', isOverlayVisible: false }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => {
|
||||||
|
const { deps, state } = createDeps({
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||||
|
isVisibleOverlayVisible: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatchMpvProtocolMessage(
|
||||||
|
{ event: 'property-change', name: 'sub-visibility', data: 'yes' },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(state.commands.pop(), {
|
||||||
|
command: ['set_property', 'sub-visibility', 'no'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dispatchMpvProtocolMessage enforces secondary sub-visibility hidden when overlay suppression is enabled', async () => {
|
||||||
|
const { deps, state } = createDeps({
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||||
|
isVisibleOverlayVisible: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatchMpvProtocolMessage(
|
||||||
|
{ event: 'property-change', name: 'secondary-sub-visibility', data: 'yes' },
|
||||||
|
deps,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(state.commands.pop(), {
|
||||||
|
command: ['set_property', 'secondary-sub-visibility', 'no'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('dispatchMpvProtocolMessage sets secondary subtitle track based on track list response', async () => {
|
test('dispatchMpvProtocolMessage sets secondary subtitle track based on track list response', async () => {
|
||||||
const { deps, state } = createDeps();
|
const { deps, state } = createDeps();
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export interface MpvProtocolHandleMessageDeps {
|
|||||||
};
|
};
|
||||||
getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
|
getSubtitleMetrics: () => MpvSubtitleRenderMetrics;
|
||||||
isVisibleOverlayVisible: () => boolean;
|
isVisibleOverlayVisible: () => boolean;
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility?: () => boolean;
|
||||||
emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void;
|
emitSubtitleChange: (payload: { text: string; isOverlayVisible: boolean }) => void;
|
||||||
emitSubtitleAssChange: (payload: { text: string }) => void;
|
emitSubtitleAssChange: (payload: { text: string }) => void;
|
||||||
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
|
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
|
||||||
@@ -216,6 +217,22 @@ export async function dispatchMpvProtocolMessage(
|
|||||||
deps.emitSubtitleMetricsChange({
|
deps.emitSubtitleMetricsChange({
|
||||||
subScaleByWindow: asBoolean(msg.data, deps.getSubtitleMetrics().subScaleByWindow),
|
subScaleByWindow: asBoolean(msg.data, deps.getSubtitleMetrics().subScaleByWindow),
|
||||||
});
|
});
|
||||||
|
} else if (msg.name === 'sub-visibility') {
|
||||||
|
if (
|
||||||
|
deps.shouldBindVisibleOverlayToMpvSubVisibility?.() &&
|
||||||
|
deps.isVisibleOverlayVisible() &&
|
||||||
|
asBoolean(msg.data, false)
|
||||||
|
) {
|
||||||
|
deps.sendCommand({ command: ['set_property', 'sub-visibility', 'no'] });
|
||||||
|
}
|
||||||
|
} else if (msg.name === 'secondary-sub-visibility') {
|
||||||
|
if (
|
||||||
|
deps.shouldBindVisibleOverlayToMpvSubVisibility?.() &&
|
||||||
|
deps.isVisibleOverlayVisible() &&
|
||||||
|
asBoolean(msg.data, false)
|
||||||
|
) {
|
||||||
|
deps.sendCommand({ command: ['set_property', 'secondary-sub-visibility', 'no'] });
|
||||||
|
}
|
||||||
} else if (msg.name === 'sub-use-margins') {
|
} else if (msg.name === 'sub-use-margins') {
|
||||||
deps.emitSubtitleMetricsChange({
|
deps.emitSubtitleMetricsChange({
|
||||||
subUseMargins: asBoolean(msg.data, deps.getSubtitleMetrics().subUseMargins),
|
subUseMargins: asBoolean(msg.data, deps.getSubtitleMetrics().subUseMargins),
|
||||||
|
|||||||
@@ -306,6 +306,32 @@ test('MpvIpcClient reconnect replays property subscriptions and initial state re
|
|||||||
assert.equal(hasPathRequest, true);
|
assert.equal(hasPathRequest, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('MpvIpcClient connect does not force primary subtitle visibility from binding path', () => {
|
||||||
|
const commands: unknown[] = [];
|
||||||
|
const client = new MpvIpcClient(
|
||||||
|
'/tmp/mpv.sock',
|
||||||
|
makeDeps({
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||||
|
isVisibleOverlayVisible: () => true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
(client as any).send = (command: unknown) => {
|
||||||
|
commands.push(command);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const callbacks = (client as any).transport.callbacks;
|
||||||
|
callbacks.onConnect();
|
||||||
|
|
||||||
|
const hasPrimaryVisibilityMutation = commands.some(
|
||||||
|
(command) =>
|
||||||
|
Array.isArray((command as { command: unknown[] }).command) &&
|
||||||
|
(command as { command: unknown[] }).command[0] === 'set_property' &&
|
||||||
|
(command as { command: unknown[] }).command[1] === 'sub-visibility',
|
||||||
|
);
|
||||||
|
assert.equal(hasPrimaryVisibilityMutation, false);
|
||||||
|
});
|
||||||
|
|
||||||
test('MpvIpcClient captures and disables secondary subtitle visibility on request', async () => {
|
test('MpvIpcClient captures and disables secondary subtitle visibility on request', async () => {
|
||||||
const commands: unknown[] = [];
|
const commands: unknown[] = [];
|
||||||
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
const client = new MpvIpcClient('/tmp/mpv.sock', makeDeps());
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export interface MpvRuntimeClientLike {
|
|||||||
replayCurrentSubtitle?: () => void;
|
replayCurrentSubtitle?: () => void;
|
||||||
playNextSubtitle?: () => void;
|
playNextSubtitle?: () => void;
|
||||||
setSubVisibility?: (visible: boolean) => void;
|
setSubVisibility?: (visible: boolean) => void;
|
||||||
|
setSecondarySubVisibility?: (visible: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showMpvOsdRuntime(
|
export function showMpvOsdRuntime(
|
||||||
@@ -84,6 +85,14 @@ export function setMpvSubVisibilityRuntime(
|
|||||||
mpvClient.setSubVisibility(visible);
|
mpvClient.setSubVisibility(visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMpvSecondarySubVisibilityRuntime(
|
||||||
|
mpvClient: MpvRuntimeClientLike | null,
|
||||||
|
visible: boolean,
|
||||||
|
): void {
|
||||||
|
if (!mpvClient?.setSecondarySubVisibility) return;
|
||||||
|
mpvClient.setSecondarySubVisibility(visible);
|
||||||
|
}
|
||||||
|
|
||||||
export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from './mpv-protocol';
|
export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from './mpv-protocol';
|
||||||
|
|
||||||
export interface MpvIpcClientProtocolDeps {
|
export interface MpvIpcClientProtocolDeps {
|
||||||
@@ -181,8 +190,6 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.deps.setOverlayVisible(true);
|
this.deps.setOverlayVisible(true);
|
||||||
}, 100);
|
}, 100);
|
||||||
} else if (this.deps.shouldBindVisibleOverlayToMpvSubVisibility()) {
|
|
||||||
this.setSubVisibility(!this.deps.isVisibleOverlayVisible());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.firstConnection = false;
|
this.firstConnection = false;
|
||||||
@@ -290,6 +297,8 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
getResolvedConfig: () => this.deps.getResolvedConfig(),
|
getResolvedConfig: () => this.deps.getResolvedConfig(),
|
||||||
getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics,
|
getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics,
|
||||||
isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(),
|
isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(),
|
||||||
|
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||||
|
this.deps.shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||||
emitSubtitleChange: (payload) => {
|
emitSubtitleChange: (payload) => {
|
||||||
this.emit('subtitle-change', payload);
|
this.emit('subtitle-change', payload);
|
||||||
},
|
},
|
||||||
@@ -488,7 +497,7 @@ export class MpvIpcClient implements MpvClient {
|
|||||||
this.previousSecondarySubVisibility = null;
|
this.previousSecondarySubVisibility = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSecondarySubVisibility(visible: boolean): void {
|
setSecondarySubVisibility(visible: boolean): void {
|
||||||
this.send({
|
this.send({
|
||||||
command: ['set_property', 'secondary-sub-visibility', visible ? 'yes' : 'no'],
|
command: ['set_property', 'secondary-sub-visibility', visible ? 'yes' : 'no'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ import {
|
|||||||
test('overlay manager initializes with empty windows and hidden overlays', () => {
|
test('overlay manager initializes with empty windows and hidden overlays', () => {
|
||||||
const manager = createOverlayManager();
|
const manager = createOverlayManager();
|
||||||
assert.equal(manager.getMainWindow(), null);
|
assert.equal(manager.getMainWindow(), null);
|
||||||
assert.equal(manager.getInvisibleWindow(), null);
|
|
||||||
assert.equal(manager.getSecondaryWindow(), null);
|
|
||||||
assert.equal(manager.getModalWindow(), null);
|
assert.equal(manager.getModalWindow(), null);
|
||||||
assert.equal(manager.getVisibleOverlayVisible(), false);
|
assert.equal(manager.getVisibleOverlayVisible(), false);
|
||||||
assert.equal(manager.getInvisibleOverlayVisible(), false);
|
|
||||||
assert.deepEqual(manager.getOverlayWindows(), []);
|
assert.deepEqual(manager.getOverlayWindows(), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,28 +19,17 @@ test('overlay manager stores window references and returns stable window order',
|
|||||||
const visibleWindow = {
|
const visibleWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
const invisibleWindow = {
|
|
||||||
isDestroyed: () => false,
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
const secondaryWindow = {
|
|
||||||
isDestroyed: () => false,
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
const modalWindow = {
|
const modalWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
|
|
||||||
manager.setMainWindow(visibleWindow);
|
manager.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
|
||||||
manager.setModalWindow(modalWindow);
|
manager.setModalWindow(modalWindow);
|
||||||
|
|
||||||
assert.equal(manager.getMainWindow(), visibleWindow);
|
assert.equal(manager.getMainWindow(), visibleWindow);
|
||||||
assert.equal(manager.getInvisibleWindow(), invisibleWindow);
|
|
||||||
assert.equal(manager.getSecondaryWindow(), secondaryWindow);
|
|
||||||
assert.equal(manager.getModalWindow(), modalWindow);
|
assert.equal(manager.getModalWindow(), modalWindow);
|
||||||
assert.equal(manager.getOverlayWindow('visible'), visibleWindow);
|
assert.equal(manager.getOverlayWindow(), visibleWindow);
|
||||||
assert.equal(manager.getOverlayWindow('invisible'), invisibleWindow);
|
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow]);
|
||||||
assert.deepEqual(manager.getOverlayWindows(), [visibleWindow, invisibleWindow, secondaryWindow]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('overlay manager excludes destroyed windows', () => {
|
test('overlay manager excludes destroyed windows', () => {
|
||||||
@@ -51,26 +37,18 @@ test('overlay manager excludes destroyed windows', () => {
|
|||||||
manager.setMainWindow({
|
manager.setMainWindow({
|
||||||
isDestroyed: () => true,
|
isDestroyed: () => true,
|
||||||
} as unknown as Electron.BrowserWindow);
|
} as unknown as Electron.BrowserWindow);
|
||||||
manager.setInvisibleWindow({
|
|
||||||
isDestroyed: () => false,
|
|
||||||
} as unknown as Electron.BrowserWindow);
|
|
||||||
manager.setSecondaryWindow({
|
|
||||||
isDestroyed: () => true,
|
|
||||||
} as unknown as Electron.BrowserWindow);
|
|
||||||
manager.setModalWindow({
|
manager.setModalWindow({
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
} as unknown as Electron.BrowserWindow);
|
} as unknown as Electron.BrowserWindow);
|
||||||
|
|
||||||
assert.equal(manager.getOverlayWindows().length, 1);
|
assert.equal(manager.getOverlayWindows().length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('overlay manager stores visibility state', () => {
|
test('overlay manager stores visibility state', () => {
|
||||||
const manager = createOverlayManager();
|
const manager = createOverlayManager();
|
||||||
|
|
||||||
manager.setVisibleOverlayVisible(true);
|
manager.setVisibleOverlayVisible(true);
|
||||||
manager.setInvisibleOverlayVisible(true);
|
|
||||||
assert.equal(manager.getVisibleOverlayVisible(), true);
|
assert.equal(manager.getVisibleOverlayVisible(), true);
|
||||||
assert.equal(manager.getInvisibleOverlayVisible(), true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('overlay manager broadcasts to non-destroyed windows', () => {
|
test('overlay manager broadcasts to non-destroyed windows', () => {
|
||||||
@@ -84,58 +62,25 @@ test('overlay manager broadcasts to non-destroyed windows', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
const deadWindow = {
|
|
||||||
isDestroyed: () => true,
|
|
||||||
webContents: {
|
|
||||||
send: (..._args: unknown[]) => {},
|
|
||||||
},
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
const secondaryWindow = {
|
|
||||||
isDestroyed: () => false,
|
|
||||||
webContents: {
|
|
||||||
send: (...args: unknown[]) => {
|
|
||||||
calls.push(args);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
|
|
||||||
manager.setMainWindow(aliveWindow);
|
manager.setMainWindow(aliveWindow);
|
||||||
manager.setInvisibleWindow(deadWindow);
|
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
|
||||||
manager.setModalWindow({
|
manager.setModalWindow({
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
webContents: { send: () => {} },
|
webContents: { send: () => {} },
|
||||||
} as unknown as Electron.BrowserWindow);
|
} as unknown as Electron.BrowserWindow);
|
||||||
manager.broadcastToOverlayWindows('x', 1, 'a');
|
manager.broadcastToOverlayWindows('x', 1, 'a');
|
||||||
|
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [['x', 1, 'a']]);
|
||||||
['x', 1, 'a'],
|
|
||||||
['x', 1, 'a'],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('overlay manager applies bounds by layer', () => {
|
test('overlay manager applies bounds for main and modal windows', () => {
|
||||||
const manager = createOverlayManager();
|
const manager = createOverlayManager();
|
||||||
const visibleCalls: Electron.Rectangle[] = [];
|
const visibleCalls: Electron.Rectangle[] = [];
|
||||||
const invisibleCalls: Electron.Rectangle[] = [];
|
|
||||||
const visibleWindow = {
|
const visibleWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
setBounds: (bounds: Electron.Rectangle) => {
|
setBounds: (bounds: Electron.Rectangle) => {
|
||||||
visibleCalls.push(bounds);
|
visibleCalls.push(bounds);
|
||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
const invisibleWindow = {
|
|
||||||
isDestroyed: () => false,
|
|
||||||
setBounds: (bounds: Electron.Rectangle) => {
|
|
||||||
invisibleCalls.push(bounds);
|
|
||||||
},
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
const secondaryWindow = {
|
|
||||||
isDestroyed: () => false,
|
|
||||||
setBounds: (bounds: Electron.Rectangle) => {
|
|
||||||
invisibleCalls.push(bounds);
|
|
||||||
},
|
|
||||||
} as unknown as Electron.BrowserWindow;
|
|
||||||
const modalCalls: Electron.Rectangle[] = [];
|
const modalCalls: Electron.Rectangle[] = [];
|
||||||
const modalWindow = {
|
const modalWindow = {
|
||||||
isDestroyed: () => false,
|
isDestroyed: () => false,
|
||||||
@@ -144,28 +89,14 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
},
|
},
|
||||||
} as unknown as Electron.BrowserWindow;
|
} as unknown as Electron.BrowserWindow;
|
||||||
manager.setMainWindow(visibleWindow);
|
manager.setMainWindow(visibleWindow);
|
||||||
manager.setInvisibleWindow(invisibleWindow);
|
|
||||||
manager.setSecondaryWindow(secondaryWindow);
|
|
||||||
manager.setModalWindow(modalWindow);
|
manager.setModalWindow(modalWindow);
|
||||||
|
|
||||||
manager.setOverlayWindowBounds('visible', {
|
manager.setOverlayWindowBounds({
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 20,
|
y: 20,
|
||||||
width: 30,
|
width: 30,
|
||||||
height: 40,
|
height: 40,
|
||||||
});
|
});
|
||||||
manager.setOverlayWindowBounds('invisible', {
|
|
||||||
x: 1,
|
|
||||||
y: 2,
|
|
||||||
width: 3,
|
|
||||||
height: 4,
|
|
||||||
});
|
|
||||||
manager.setSecondaryWindowBounds({
|
|
||||||
x: 8,
|
|
||||||
y: 9,
|
|
||||||
width: 10,
|
|
||||||
height: 11,
|
|
||||||
});
|
|
||||||
manager.setModalWindowBounds({
|
manager.setModalWindowBounds({
|
||||||
x: 80,
|
x: 80,
|
||||||
y: 90,
|
y: 90,
|
||||||
@@ -174,14 +105,10 @@ test('overlay manager applies bounds by layer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(visibleCalls, [{ x: 10, y: 20, width: 30, height: 40 }]);
|
assert.deepEqual(visibleCalls, [{ x: 10, y: 20, width: 30, height: 40 }]);
|
||||||
assert.deepEqual(invisibleCalls, [
|
|
||||||
{ x: 1, y: 2, width: 3, height: 4 },
|
|
||||||
{ x: 8, y: 9, width: 10, height: 11 },
|
|
||||||
]);
|
|
||||||
assert.deepEqual(modalCalls, [{ x: 80, y: 90, width: 100, height: 110 }]);
|
assert.deepEqual(modalCalls, [{ x: 80, y: 90, width: 100, height: 110 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('runtime-option and debug broadcasts use expected channels', () => {
|
test('runtime-option broadcast still uses expected channel', () => {
|
||||||
const broadcasts: unknown[][] = [];
|
const broadcasts: unknown[][] = [];
|
||||||
broadcastRuntimeOptionsChangedRuntime(
|
broadcastRuntimeOptionsChangedRuntime(
|
||||||
() => [],
|
() => [],
|
||||||
@@ -196,14 +123,8 @@ test('runtime-option and debug broadcasts use expected channels', () => {
|
|||||||
(enabled) => {
|
(enabled) => {
|
||||||
state = enabled;
|
state = enabled;
|
||||||
},
|
},
|
||||||
(channel, ...args) => {
|
|
||||||
broadcasts.push([channel, ...args]);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
assert.equal(changed, true);
|
assert.equal(changed, true);
|
||||||
assert.equal(state, true);
|
assert.equal(state, true);
|
||||||
assert.deepEqual(broadcasts, [
|
assert.deepEqual(broadcasts, [['runtime-options:changed', []]]);
|
||||||
['runtime-options:changed', []],
|
|
||||||
['overlay-debug-visualization:set', true],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,60 +2,37 @@ import type { BrowserWindow } from 'electron';
|
|||||||
import { RuntimeOptionState, WindowGeometry } from '../../types';
|
import { RuntimeOptionState, WindowGeometry } from '../../types';
|
||||||
import { updateOverlayWindowBounds } from './overlay-window';
|
import { updateOverlayWindowBounds } from './overlay-window';
|
||||||
|
|
||||||
type OverlayLayer = 'visible' | 'invisible';
|
|
||||||
|
|
||||||
export interface OverlayManager {
|
export interface OverlayManager {
|
||||||
getMainWindow: () => BrowserWindow | null;
|
getMainWindow: () => BrowserWindow | null;
|
||||||
setMainWindow: (window: BrowserWindow | null) => void;
|
setMainWindow: (window: BrowserWindow | null) => void;
|
||||||
getInvisibleWindow: () => BrowserWindow | null;
|
|
||||||
setInvisibleWindow: (window: BrowserWindow | null) => void;
|
|
||||||
getSecondaryWindow: () => BrowserWindow | null;
|
|
||||||
setSecondaryWindow: (window: BrowserWindow | null) => void;
|
|
||||||
getModalWindow: () => BrowserWindow | null;
|
getModalWindow: () => BrowserWindow | null;
|
||||||
setModalWindow: (window: BrowserWindow | null) => void;
|
setModalWindow: (window: BrowserWindow | null) => void;
|
||||||
getOverlayWindow: (layer: OverlayLayer) => BrowserWindow | null;
|
getOverlayWindow: () => BrowserWindow | null;
|
||||||
setOverlayWindowBounds: (layer: OverlayLayer, geometry: WindowGeometry) => void;
|
setOverlayWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
setSecondaryWindowBounds: (geometry: WindowGeometry) => void;
|
|
||||||
setModalWindowBounds: (geometry: WindowGeometry) => void;
|
setModalWindowBounds: (geometry: WindowGeometry) => void;
|
||||||
getVisibleOverlayVisible: () => boolean;
|
getVisibleOverlayVisible: () => boolean;
|
||||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||||
getInvisibleOverlayVisible: () => boolean;
|
|
||||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
|
||||||
getOverlayWindows: () => BrowserWindow[];
|
getOverlayWindows: () => BrowserWindow[];
|
||||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createOverlayManager(): OverlayManager {
|
export function createOverlayManager(): OverlayManager {
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
let invisibleWindow: BrowserWindow | null = null;
|
|
||||||
let secondaryWindow: BrowserWindow | null = null;
|
|
||||||
let modalWindow: BrowserWindow | null = null;
|
let modalWindow: BrowserWindow | null = null;
|
||||||
let visibleOverlayVisible = false;
|
let visibleOverlayVisible = false;
|
||||||
let invisibleOverlayVisible = false;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getMainWindow: () => mainWindow,
|
getMainWindow: () => mainWindow,
|
||||||
setMainWindow: (window) => {
|
setMainWindow: (window) => {
|
||||||
mainWindow = window;
|
mainWindow = window;
|
||||||
},
|
},
|
||||||
getInvisibleWindow: () => invisibleWindow,
|
|
||||||
setInvisibleWindow: (window) => {
|
|
||||||
invisibleWindow = window;
|
|
||||||
},
|
|
||||||
getSecondaryWindow: () => secondaryWindow,
|
|
||||||
setSecondaryWindow: (window) => {
|
|
||||||
secondaryWindow = window;
|
|
||||||
},
|
|
||||||
getModalWindow: () => modalWindow,
|
getModalWindow: () => modalWindow,
|
||||||
setModalWindow: (window) => {
|
setModalWindow: (window) => {
|
||||||
modalWindow = window;
|
modalWindow = window;
|
||||||
},
|
},
|
||||||
getOverlayWindow: (layer) => (layer === 'visible' ? mainWindow : invisibleWindow),
|
getOverlayWindow: () => mainWindow,
|
||||||
setOverlayWindowBounds: (layer, geometry) => {
|
setOverlayWindowBounds: (geometry) => {
|
||||||
updateOverlayWindowBounds(geometry, layer === 'visible' ? mainWindow : invisibleWindow);
|
updateOverlayWindowBounds(geometry, mainWindow);
|
||||||
},
|
|
||||||
setSecondaryWindowBounds: (geometry) => {
|
|
||||||
updateOverlayWindowBounds(geometry, secondaryWindow);
|
|
||||||
},
|
},
|
||||||
setModalWindowBounds: (geometry) => {
|
setModalWindowBounds: (geometry) => {
|
||||||
updateOverlayWindowBounds(geometry, modalWindow);
|
updateOverlayWindowBounds(geometry, modalWindow);
|
||||||
@@ -64,36 +41,12 @@ export function createOverlayManager(): OverlayManager {
|
|||||||
setVisibleOverlayVisible: (visible) => {
|
setVisibleOverlayVisible: (visible) => {
|
||||||
visibleOverlayVisible = visible;
|
visibleOverlayVisible = visible;
|
||||||
},
|
},
|
||||||
getInvisibleOverlayVisible: () => invisibleOverlayVisible,
|
|
||||||
setInvisibleOverlayVisible: (visible) => {
|
|
||||||
invisibleOverlayVisible = visible;
|
|
||||||
},
|
|
||||||
getOverlayWindows: () => {
|
getOverlayWindows: () => {
|
||||||
const windows: BrowserWindow[] = [];
|
return mainWindow && !mainWindow.isDestroyed() ? [mainWindow] : [];
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
windows.push(mainWindow);
|
|
||||||
}
|
|
||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
|
||||||
windows.push(invisibleWindow);
|
|
||||||
}
|
|
||||||
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
|
||||||
windows.push(secondaryWindow);
|
|
||||||
}
|
|
||||||
return windows;
|
|
||||||
},
|
},
|
||||||
broadcastToOverlayWindows: (channel, ...args) => {
|
broadcastToOverlayWindows: (channel, ...args) => {
|
||||||
const windows: BrowserWindow[] = [];
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
windows.push(mainWindow);
|
mainWindow.webContents.send(channel, ...args);
|
||||||
}
|
|
||||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
|
||||||
windows.push(invisibleWindow);
|
|
||||||
}
|
|
||||||
if (secondaryWindow && !secondaryWindow.isDestroyed()) {
|
|
||||||
windows.push(secondaryWindow);
|
|
||||||
}
|
|
||||||
for (const window of windows) {
|
|
||||||
window.webContents.send(channel, ...args);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -110,10 +63,8 @@ export function setOverlayDebugVisualizationEnabledRuntime(
|
|||||||
currentEnabled: boolean,
|
currentEnabled: boolean,
|
||||||
nextEnabled: boolean,
|
nextEnabled: boolean,
|
||||||
setState: (enabled: boolean) => void,
|
setState: (enabled: boolean) => void,
|
||||||
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void,
|
|
||||||
): boolean {
|
): boolean {
|
||||||
if (currentEnabled === nextEnabled) return false;
|
if (currentEnabled === nextEnabled) return false;
|
||||||
setState(nextEnabled);
|
setState(nextEnabled);
|
||||||
broadcastToOverlayWindows('overlay-debug-visualization:set', nextEnabled);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import type { WindowGeometry } from '../../types';
|
|
||||||
|
|
||||||
export const SECONDARY_OVERLAY_MAX_HEIGHT_RATIO = 0.2;
|
|
||||||
|
|
||||||
function toInteger(value: number): number {
|
|
||||||
return Number.isFinite(value) ? Math.round(value) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampPositive(value: number): number {
|
|
||||||
return Math.max(1, toInteger(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function splitOverlayGeometryForSecondaryBar(geometry: WindowGeometry): {
|
|
||||||
secondary: WindowGeometry;
|
|
||||||
primary: WindowGeometry;
|
|
||||||
} {
|
|
||||||
const x = toInteger(geometry.x);
|
|
||||||
const y = toInteger(geometry.y);
|
|
||||||
const width = clampPositive(geometry.width);
|
|
||||||
const totalHeight = clampPositive(geometry.height);
|
|
||||||
|
|
||||||
const secondaryHeight = clampPositive(
|
|
||||||
Math.min(totalHeight, Math.round(totalHeight * SECONDARY_OVERLAY_MAX_HEIGHT_RATIO)),
|
|
||||||
);
|
|
||||||
const primaryHeight = clampPositive(totalHeight - secondaryHeight);
|
|
||||||
|
|
||||||
return {
|
|
||||||
secondary: {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height: secondaryHeight,
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
x,
|
|
||||||
y: y + secondaryHeight,
|
|
||||||
width,
|
|
||||||
height: primaryHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import test from 'node:test';
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { splitOverlayGeometryForSecondaryBar } from './overlay-window-geometry';
|
|
||||||
|
|
||||||
test('splitOverlayGeometryForSecondaryBar returns 20/80 top-bottom regions', () => {
|
|
||||||
const regions = splitOverlayGeometryForSecondaryBar({
|
|
||||||
x: 100,
|
|
||||||
y: 50,
|
|
||||||
width: 1200,
|
|
||||||
height: 900,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(regions.secondary, {
|
|
||||||
x: 100,
|
|
||||||
y: 50,
|
|
||||||
width: 1200,
|
|
||||||
height: 180,
|
|
||||||
});
|
|
||||||
assert.deepEqual(regions.primary, {
|
|
||||||
x: 100,
|
|
||||||
y: 230,
|
|
||||||
width: 1200,
|
|
||||||
height: 720,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('splitOverlayGeometryForSecondaryBar keeps positive sizes for tiny heights', () => {
|
|
||||||
const regions = splitOverlayGeometryForSecondaryBar({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 300,
|
|
||||||
height: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.ok(regions.secondary.height >= 1);
|
|
||||||
assert.ok(regions.primary.height >= 1);
|
|
||||||
});
|
|
||||||
@@ -4,8 +4,25 @@ import { WindowGeometry } from '../../types';
|
|||||||
import { createLogger } from '../../logger';
|
import { createLogger } from '../../logger';
|
||||||
|
|
||||||
const logger = createLogger('main:overlay-window');
|
const logger = createLogger('main:overlay-window');
|
||||||
|
const overlayWindowLayerByInstance = new WeakMap<BrowserWindow, OverlayWindowKind>();
|
||||||
|
|
||||||
export type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
|
function getOverlayWindowHtmlPath(): string {
|
||||||
|
return path.join(__dirname, '..', '..', 'renderer', 'index.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOverlayWindowLayer(window: BrowserWindow, layer: OverlayWindowKind): void {
|
||||||
|
overlayWindowLayerByInstance.set(window, layer);
|
||||||
|
const htmlPath = getOverlayWindowHtmlPath();
|
||||||
|
window
|
||||||
|
.loadFile(htmlPath, {
|
||||||
|
query: { layer },
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error('Failed to load HTML file:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OverlayWindowKind = 'visible' | 'modal';
|
||||||
|
|
||||||
export function updateOverlayWindowBounds(
|
export function updateOverlayWindowBounds(
|
||||||
geometry: WindowGeometry,
|
geometry: WindowGeometry,
|
||||||
@@ -32,14 +49,11 @@ export function ensureOverlayWindowLevel(window: BrowserWindow): void {
|
|||||||
|
|
||||||
export function enforceOverlayLayerOrder(options: {
|
export function enforceOverlayLayerOrder(options: {
|
||||||
visibleOverlayVisible: boolean;
|
visibleOverlayVisible: boolean;
|
||||||
invisibleOverlayVisible: boolean;
|
|
||||||
mainWindow: BrowserWindow | null;
|
mainWindow: BrowserWindow | null;
|
||||||
invisibleWindow: BrowserWindow | null;
|
|
||||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
}): void {
|
}): void {
|
||||||
if (!options.visibleOverlayVisible || !options.invisibleOverlayVisible) return;
|
if (!options.visibleOverlayVisible) return;
|
||||||
if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
|
if (!options.mainWindow || options.mainWindow.isDestroyed()) return;
|
||||||
if (!options.invisibleWindow || options.invisibleWindow.isDestroyed()) return;
|
|
||||||
|
|
||||||
options.ensureOverlayWindowLevel(options.mainWindow);
|
options.ensureOverlayWindowLevel(options.mainWindow);
|
||||||
options.mainWindow.moveTop();
|
options.mainWindow.moveTop();
|
||||||
@@ -49,7 +63,6 @@ export function createOverlayWindow(
|
|||||||
kind: OverlayWindowKind,
|
kind: OverlayWindowKind,
|
||||||
options: {
|
options: {
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
overlayDebugVisualizationEnabled: boolean;
|
|
||||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
@@ -83,16 +96,7 @@ export function createOverlayWindow(
|
|||||||
});
|
});
|
||||||
|
|
||||||
options.ensureOverlayWindowLevel(window);
|
options.ensureOverlayWindowLevel(window);
|
||||||
|
loadOverlayWindowLayer(window, kind);
|
||||||
const htmlPath = path.join(__dirname, '..', '..', 'renderer', 'index.html');
|
|
||||||
|
|
||||||
window
|
|
||||||
.loadFile(htmlPath, {
|
|
||||||
query: { layer: kind },
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error('Failed to load HTML file:', err);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => {
|
||||||
logger.error('Page failed to load:', errorCode, errorDescription, validatedURL);
|
logger.error('Page failed to load:', errorCode, errorDescription, validatedURL);
|
||||||
@@ -100,10 +104,6 @@ export function createOverlayWindow(
|
|||||||
|
|
||||||
window.webContents.on('did-finish-load', () => {
|
window.webContents.on('did-finish-load', () => {
|
||||||
options.onRuntimeOptionsChanged();
|
options.onRuntimeOptionsChanged();
|
||||||
window.webContents.send(
|
|
||||||
'overlay-debug-visualization:set',
|
|
||||||
options.overlayDebugVisualizationEnabled,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (kind === 'visible') {
|
if (kind === 'visible') {
|
||||||
@@ -140,3 +140,9 @@ export function createOverlayWindow(
|
|||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function syncOverlayWindowLayer(window: BrowserWindow, layer: 'visible'): void {
|
||||||
|
if (window.isDestroyed()) return;
|
||||||
|
if (overlayWindowLayerByInstance.get(window) === layer) return;
|
||||||
|
loadOverlayWindowLayer(window, layer);
|
||||||
|
}
|
||||||
|
|||||||
309
src/main.ts
309
src/main.ts
@@ -218,7 +218,6 @@ import {
|
|||||||
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
|
createBuildSetOverlayDebugVisualizationEnabledMainDepsHandler,
|
||||||
createBuildEnforceOverlayLayerOrderMainDepsHandler,
|
createBuildEnforceOverlayLayerOrderMainDepsHandler,
|
||||||
createBuildEnsureOverlayWindowLevelMainDepsHandler,
|
createBuildEnsureOverlayWindowLevelMainDepsHandler,
|
||||||
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler,
|
|
||||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
|
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
|
||||||
createOverlayWindowRuntimeHandlers,
|
createOverlayWindowRuntimeHandlers,
|
||||||
createOverlayRuntimeBootstrapHandlers,
|
createOverlayRuntimeBootstrapHandlers,
|
||||||
@@ -234,7 +233,6 @@ import {
|
|||||||
createSetOverlayDebugVisualizationEnabledHandler,
|
createSetOverlayDebugVisualizationEnabledHandler,
|
||||||
createEnforceOverlayLayerOrderHandler,
|
createEnforceOverlayLayerOrderHandler,
|
||||||
createEnsureOverlayWindowLevelHandler,
|
createEnsureOverlayWindowLevelHandler,
|
||||||
createUpdateInvisibleOverlayBoundsHandler,
|
|
||||||
createUpdateVisibleOverlayBoundsHandler,
|
createUpdateVisibleOverlayBoundsHandler,
|
||||||
createLoadSubtitlePositionHandler,
|
createLoadSubtitlePositionHandler,
|
||||||
createSaveSubtitlePositionHandler,
|
createSaveSubtitlePositionHandler,
|
||||||
@@ -356,16 +354,16 @@ import {
|
|||||||
runStartupBootstrapRuntime,
|
runStartupBootstrapRuntime,
|
||||||
saveSubtitlePosition as saveSubtitlePositionCore,
|
saveSubtitlePosition as saveSubtitlePositionCore,
|
||||||
sendMpvCommandRuntime,
|
sendMpvCommandRuntime,
|
||||||
setInvisibleOverlayVisible as setInvisibleOverlayVisibleCore,
|
setMpvSecondarySubVisibilityRuntime,
|
||||||
setMpvSubVisibilityRuntime,
|
setMpvSubVisibilityRuntime,
|
||||||
setOverlayDebugVisualizationEnabledRuntime,
|
setOverlayDebugVisualizationEnabledRuntime,
|
||||||
|
syncOverlayWindowLayer,
|
||||||
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
setVisibleOverlayVisible as setVisibleOverlayVisibleCore,
|
||||||
showMpvOsdRuntime,
|
showMpvOsdRuntime,
|
||||||
tokenizeSubtitle as tokenizeSubtitleCore,
|
tokenizeSubtitle as tokenizeSubtitleCore,
|
||||||
triggerFieldGrouping as triggerFieldGroupingCore,
|
triggerFieldGrouping as triggerFieldGroupingCore,
|
||||||
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
updateLastCardFromClipboard as updateLastCardFromClipboardCore,
|
||||||
} from './core/services';
|
} from './core/services';
|
||||||
import { splitOverlayGeometryForSecondaryBar } from './core/services/overlay-window-geometry';
|
|
||||||
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
import { createAnilistUpdateQueue } from './core/services/anilist/anilist-update-queue';
|
||||||
import {
|
import {
|
||||||
guessAnilistMediaInfo,
|
guessAnilistMediaInfo,
|
||||||
@@ -376,7 +374,10 @@ import { applyRuntimeOptionResultRuntime } from './core/services/runtime-options
|
|||||||
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
import { createAnilistTokenStore } from './core/services/anilist/anilist-token-store';
|
||||||
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
import { createBuildOverlayShortcutsRuntimeMainDepsHandler } from './main/runtime/domains/shortcuts';
|
||||||
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
import { createMainRuntimeRegistry } from './main/runtime/registry';
|
||||||
import { createApplyHoveredTokenOverlayHandler } from './main/runtime/mpv-hover-highlight';
|
import {
|
||||||
|
createEnsureOverlayMpvSubtitlesHiddenHandler,
|
||||||
|
createRestoreOverlayMpvSubtitlesHandler,
|
||||||
|
} from './main/runtime/overlay-mpv-sub-visibility';
|
||||||
import {
|
import {
|
||||||
composeAnilistSetupHandlers,
|
composeAnilistSetupHandlers,
|
||||||
composeAnilistTrackingHandlers,
|
composeAnilistTrackingHandlers,
|
||||||
@@ -644,7 +645,6 @@ const buildOverlayContentMeasurementStoreMainDepsHandler =
|
|||||||
});
|
});
|
||||||
const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
const buildOverlayModalRuntimeMainDepsHandler = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
getModalWindow: () => overlayManager.getModalWindow(),
|
getModalWindow: () => overlayManager.getModalWindow(),
|
||||||
createModalWindow: () => createModalWindow(),
|
createModalWindow: () => createModalWindow(),
|
||||||
getModalGeometry: () => getCurrentOverlayGeometry(),
|
getModalGeometry: () => getCurrentOverlayGeometry(),
|
||||||
@@ -725,13 +725,70 @@ async function initializeDiscordPresenceService(): Promise<void> {
|
|||||||
await appState.discordPresenceService.start();
|
await appState.discordPresenceService.start();
|
||||||
publishDiscordPresence();
|
publishDiscordPresence();
|
||||||
}
|
}
|
||||||
const applyHoveredTokenOverlay = createApplyHoveredTokenOverlayHandler({
|
const ensureOverlayMpvSubtitlesHidden = createEnsureOverlayMpvSubtitlesHiddenHandler({
|
||||||
getMpvClient: () => appState.mpvClient,
|
getMpvClient: () => appState.mpvClient,
|
||||||
getCurrentSubtitleData: () => appState.currentSubtitleData,
|
getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility,
|
||||||
getHoveredTokenIndex: () => appState.hoveredSubtitleTokenIndex,
|
setSavedSubVisibility: (visible) => {
|
||||||
getHoveredSubtitleRevision: () => appState.hoveredSubtitleRevision,
|
appState.overlaySavedMpvSubVisibility = visible;
|
||||||
getHoverTokenColor: () => getResolvedConfig().subtitleStyle.hoverTokenColor ?? null,
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => appState.overlaySavedSecondaryMpvSubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
appState.overlaySavedSecondaryMpvSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => appState.overlayMpvSubVisibilityRevision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
appState.overlayMpvSubVisibilityRevision = revision;
|
||||||
|
},
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
setMpvSubVisibilityRuntime(appState.mpvClient, visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
setMpvSecondarySubVisibilityRuntime(appState.mpvClient, visible);
|
||||||
|
},
|
||||||
|
logWarn: (message, error) => {
|
||||||
|
logger.warn(message, error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const restoreOverlayMpvSubtitles = createRestoreOverlayMpvSubtitlesHandler({
|
||||||
|
getSavedSubVisibility: () => appState.overlaySavedMpvSubVisibility,
|
||||||
|
setSavedSubVisibility: (visible) => {
|
||||||
|
appState.overlaySavedMpvSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => appState.overlaySavedSecondaryMpvSubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
appState.overlaySavedSecondaryMpvSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => appState.overlayMpvSubVisibilityRevision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
appState.overlayMpvSubVisibilityRevision = revision;
|
||||||
|
},
|
||||||
|
isMpvConnected: () => Boolean(appState.mpvClient?.connected),
|
||||||
|
shouldKeepSuppressedFromVisibleOverlayBinding: () => shouldSuppressMpvSubtitlesForOverlay(),
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
setMpvSubVisibilityRuntime(appState.mpvClient, visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
setMpvSecondarySubVisibilityRuntime(appState.mpvClient, visible);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function shouldSuppressMpvSubtitlesForOverlay(): boolean {
|
||||||
|
return (
|
||||||
|
appState.secondarySubMode === 'visible' ||
|
||||||
|
(overlayManager.getVisibleOverlayVisible() &&
|
||||||
|
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncOverlayMpvSubtitleSuppression(): void {
|
||||||
|
if (shouldSuppressMpvSubtitlesForOverlay()) {
|
||||||
|
void ensureOverlayMpvSubtitlesHidden();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreOverlayMpvSubtitles();
|
||||||
|
}
|
||||||
|
|
||||||
const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRuntimeMainDepsHandler({
|
const buildImmersionMediaRuntimeMainDepsHandler = createBuildImmersionMediaRuntimeMainDepsHandler({
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH,
|
defaultImmersionDbPath: DEFAULT_IMMERSION_DB_PATH,
|
||||||
@@ -766,7 +823,6 @@ const buildAnilistStateRuntimeMainDepsHandler = createBuildAnilistStateRuntimeMa
|
|||||||
const buildConfigDerivedRuntimeMainDepsHandler = createBuildConfigDerivedRuntimeMainDepsHandler({
|
const buildConfigDerivedRuntimeMainDepsHandler = createBuildConfigDerivedRuntimeMainDepsHandler({
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
platform: process.platform,
|
|
||||||
defaultJimakuLanguagePreference: DEFAULT_CONFIG.jimaku.languagePreference,
|
defaultJimakuLanguagePreference: DEFAULT_CONFIG.jimaku.languagePreference,
|
||||||
defaultJimakuMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults,
|
defaultJimakuMaxEntryResults: DEFAULT_CONFIG.jimaku.maxEntryResults,
|
||||||
defaultJimakuApiBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl,
|
defaultJimakuApiBaseUrl: DEFAULT_CONFIG.jimaku.apiBaseUrl,
|
||||||
@@ -801,15 +857,7 @@ const buildSubtitleProcessingControllerMainDepsHandler =
|
|||||||
return await tokenizeSubtitle(text);
|
return await tokenizeSubtitle(text);
|
||||||
},
|
},
|
||||||
emitSubtitle: (payload) => {
|
emitSubtitle: (payload) => {
|
||||||
const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
|
|
||||||
const nextSubtitleText = payload?.text ?? null;
|
|
||||||
const subtitleChanged = previousSubtitleText !== nextSubtitleText;
|
|
||||||
appState.currentSubtitleData = payload;
|
appState.currentSubtitleData = payload;
|
||||||
if (subtitleChanged) {
|
|
||||||
appState.hoveredSubtitleTokenIndex = null;
|
|
||||||
appState.hoveredSubtitleRevision += 1;
|
|
||||||
applyHoveredTokenOverlay();
|
|
||||||
}
|
|
||||||
broadcastToOverlayWindows('subtitle:set', payload);
|
broadcastToOverlayWindows('subtitle:set', payload);
|
||||||
subtitleWsService.broadcast(payload, {
|
subtitleWsService.broadcast(payload, {
|
||||||
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
|
||||||
@@ -850,7 +898,7 @@ const overlayShortcutsRuntime = createOverlayShortcutsRuntimeService(
|
|||||||
copySubtitle: () => {
|
copySubtitle: () => {
|
||||||
copyCurrentSubtitle();
|
copyCurrentSubtitle();
|
||||||
},
|
},
|
||||||
toggleSecondarySubMode: () => cycleSecondarySubMode(),
|
toggleSecondarySubMode: () => handleCycleSecondarySubMode(),
|
||||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||||
@@ -899,8 +947,7 @@ const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadApp
|
|||||||
refreshGlobalAndOverlayShortcuts();
|
refreshGlobalAndOverlayShortcuts();
|
||||||
},
|
},
|
||||||
setSecondarySubMode: (mode) => {
|
setSecondarySubMode: (mode) => {
|
||||||
appState.secondarySubMode = mode;
|
setSecondarySubMode(mode);
|
||||||
syncSecondaryOverlayWindowVisibility();
|
|
||||||
},
|
},
|
||||||
broadcastToOverlayWindows: (channel, payload) => {
|
broadcastToOverlayWindows: (channel, payload) => {
|
||||||
broadcastToOverlayWindows(channel, payload);
|
broadcastToOverlayWindows(channel, payload);
|
||||||
@@ -1023,9 +1070,7 @@ const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHos
|
|||||||
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
|
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
|
||||||
getResolver: () => getFieldGroupingResolver(),
|
getResolver: () => getFieldGroupingResolver(),
|
||||||
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
setResolver: (resolver) => setFieldGroupingResolver(resolver),
|
||||||
getRestoreVisibleOverlayOnModalClose: () =>
|
getRestoreVisibleOverlayOnModalClose: () =>
|
||||||
@@ -1067,26 +1112,40 @@ const mediaRuntime = createMediaRuntimeService(
|
|||||||
const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
const overlayVisibilityRuntime = createOverlayVisibilityRuntimeService(
|
||||||
createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
createBuildOverlayVisibilityRuntimeMainDepsHandler({
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
getWindowTracker: () => appState.windowTracker,
|
getWindowTracker: () => appState.windowTracker,
|
||||||
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
getTrackerNotReadyWarningShown: () => appState.trackerNotReadyWarningShown,
|
||||||
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
setTrackerNotReadyWarningShown: (shown: boolean) => {
|
||||||
appState.trackerNotReadyWarningShown = shown;
|
appState.trackerNotReadyWarningShown = shown;
|
||||||
},
|
},
|
||||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
||||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
|
|
||||||
updateInvisibleOverlayBounds(geometry),
|
|
||||||
ensureOverlayWindowLevel: (window) => {
|
ensureOverlayWindowLevel: (window) => {
|
||||||
ensureOverlayWindowLevel(window);
|
ensureOverlayWindowLevel(window);
|
||||||
},
|
},
|
||||||
|
syncPrimaryOverlayWindowLayer: (layer) => {
|
||||||
|
syncPrimaryOverlayWindowLayer(layer);
|
||||||
|
},
|
||||||
enforceOverlayLayerOrder: () => {
|
enforceOverlayLayerOrder: () => {
|
||||||
enforceOverlayLayerOrder();
|
enforceOverlayLayerOrder();
|
||||||
},
|
},
|
||||||
syncOverlayShortcuts: () => {
|
syncOverlayShortcuts: () => {
|
||||||
overlayShortcutsRuntime.syncOverlayShortcuts();
|
overlayShortcutsRuntime.syncOverlayShortcuts();
|
||||||
},
|
},
|
||||||
|
isMacOSPlatform: () => process.platform === 'darwin',
|
||||||
|
showOverlayLoadingOsd: (message: string) => {
|
||||||
|
showMpvOsd(message);
|
||||||
|
},
|
||||||
|
resolveFallbackBounds: () => {
|
||||||
|
const cursorPoint = screen.getCursorScreenPoint();
|
||||||
|
const display = screen.getDisplayNearestPoint(cursorPoint);
|
||||||
|
const fallbackBounds = display.workArea;
|
||||||
|
return {
|
||||||
|
x: fallbackBounds.x,
|
||||||
|
y: fallbackBounds.y,
|
||||||
|
width: fallbackBounds.width,
|
||||||
|
height: fallbackBounds.height,
|
||||||
|
};
|
||||||
|
},
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1165,7 +1224,6 @@ const buildSetOverlayDebugVisualizationEnabledMainDepsHandler =
|
|||||||
setCurrentEnabled: (next) => {
|
setCurrentEnabled: (next) => {
|
||||||
appState.overlayDebugVisualizationEnabled = next;
|
appState.overlayDebugVisualizationEnabled = next;
|
||||||
},
|
},
|
||||||
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
|
||||||
});
|
});
|
||||||
const setOverlayDebugVisualizationEnabledMainDeps =
|
const setOverlayDebugVisualizationEnabledMainDeps =
|
||||||
buildSetOverlayDebugVisualizationEnabledMainDepsHandler();
|
buildSetOverlayDebugVisualizationEnabledMainDepsHandler();
|
||||||
@@ -1826,6 +1884,9 @@ const {
|
|||||||
destroyTray: () => destroyTray(),
|
destroyTray: () => destroyTray(),
|
||||||
stopConfigHotReload: () => configHotReloadRuntime.stop(),
|
stopConfigHotReload: () => configHotReloadRuntime.stop(),
|
||||||
restorePreviousSecondarySubVisibility: () => restorePreviousSecondarySubVisibility(),
|
restorePreviousSecondarySubVisibility: () => restorePreviousSecondarySubVisibility(),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => {
|
||||||
|
restoreOverlayMpvSubtitles({ respectVisibleOverlayBinding: false });
|
||||||
|
},
|
||||||
unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(),
|
unregisterAllGlobalShortcuts: () => globalShortcut.unregisterAll(),
|
||||||
stopSubtitleWebsocket: () => subtitleWsService.stop(),
|
stopSubtitleWebsocket: () => subtitleWsService.stop(),
|
||||||
stopTexthookerService: () => texthookerService.stop(),
|
stopTexthookerService: () => texthookerService.stop(),
|
||||||
@@ -1870,14 +1931,11 @@ const {
|
|||||||
createMainWindow: () => {
|
createMainWindow: () => {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
},
|
},
|
||||||
createInvisibleWindow: () => {
|
|
||||||
createInvisibleWindow();
|
|
||||||
},
|
|
||||||
updateVisibleOverlayVisibility: () => {
|
updateVisibleOverlayVisibility: () => {
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||||
},
|
},
|
||||||
updateInvisibleOverlayVisibility: () => {
|
syncOverlayMpvSubtitleSuppression: () => {
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility();
|
syncOverlayMpvSubtitleSuppression();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1934,8 +1992,7 @@ const { reloadConfig: reloadConfigHandler, appReadyRuntimeRunner } = composeAppR
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
appState.secondarySubMode = mode;
|
setSecondarySubMode(mode);
|
||||||
syncSecondaryOverlayWindowVisibility();
|
|
||||||
},
|
},
|
||||||
defaultSecondarySubMode: 'hover',
|
defaultSecondarySubMode: 'hover',
|
||||||
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
defaultWebsocketPort: DEFAULT_CONFIG.websocket.port,
|
||||||
@@ -2124,6 +2181,9 @@ const {
|
|||||||
updateCurrentMediaPath: (path) => {
|
updateCurrentMediaPath: (path) => {
|
||||||
mediaRuntime.updateCurrentMediaPath(path);
|
mediaRuntime.updateCurrentMediaPath(path);
|
||||||
},
|
},
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => {
|
||||||
|
restoreOverlayMpvSubtitles({ respectVisibleOverlayBinding: false });
|
||||||
|
},
|
||||||
getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(),
|
getCurrentAnilistMediaKey: () => getCurrentAnilistMediaKey(),
|
||||||
resetAnilistMediaTracking: (mediaKey) => {
|
resetAnilistMediaTracking: (mediaKey) => {
|
||||||
resetAnilistMediaTracking(mediaKey);
|
resetAnilistMediaTracking(mediaKey);
|
||||||
@@ -2149,6 +2209,9 @@ const {
|
|||||||
updateSubtitleRenderMetrics: (patch) => {
|
updateSubtitleRenderMetrics: (patch) => {
|
||||||
updateMpvSubtitleRenderMetrics(patch as Partial<MpvSubtitleRenderMetrics>);
|
updateMpvSubtitleRenderMetrics(patch as Partial<MpvSubtitleRenderMetrics>);
|
||||||
},
|
},
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => {
|
||||||
|
syncOverlayMpvSubtitleSuppression();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||||
createClient: MpvIpcClient,
|
createClient: MpvIpcClient,
|
||||||
@@ -2170,8 +2233,8 @@ const {
|
|||||||
appState.mpvSubtitleRenderMetrics = metrics;
|
appState.mpvSubtitleRenderMetrics = metrics;
|
||||||
},
|
},
|
||||||
applyPatch: (current, patch) => applyMpvSubtitleRenderMetricsPatch(current, patch),
|
applyPatch: (current, patch) => applyMpvSubtitleRenderMetricsPatch(current, patch),
|
||||||
broadcastMetrics: (metrics) => {
|
broadcastMetrics: () => {
|
||||||
broadcastToOverlayWindows('mpv-subtitle-render-metrics:set', metrics);
|
// no renderer consumer for subtitle render metrics updates at present
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tokenizer: {
|
tokenizer: {
|
||||||
@@ -2276,52 +2339,21 @@ function getCurrentOverlayGeometry(): WindowGeometry {
|
|||||||
return getOverlayGeometryFallback();
|
return getOverlayGeometryFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSecondaryOverlayWindowVisibility(): void {
|
function applyOverlayRegions(geometry: WindowGeometry): void {
|
||||||
const secondaryWindow = overlayManager.getSecondaryWindow();
|
|
||||||
if (!secondaryWindow || secondaryWindow.isDestroyed()) return;
|
|
||||||
|
|
||||||
if (appState.secondarySubMode === 'hidden') {
|
|
||||||
secondaryWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
||||||
secondaryWindow.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
secondaryWindow.setIgnoreMouseEvents(false);
|
|
||||||
ensureOverlayWindowLevel(secondaryWindow);
|
|
||||||
if (typeof secondaryWindow.showInactive === 'function') {
|
|
||||||
secondaryWindow.showInactive();
|
|
||||||
} else {
|
|
||||||
secondaryWindow.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyOverlayRegions(layer: 'visible' | 'invisible', geometry: WindowGeometry): void {
|
|
||||||
lastOverlayWindowGeometry = geometry;
|
lastOverlayWindowGeometry = geometry;
|
||||||
const regions = splitOverlayGeometryForSecondaryBar(geometry);
|
overlayManager.setOverlayWindowBounds(geometry);
|
||||||
overlayManager.setOverlayWindowBounds(layer, regions.primary);
|
|
||||||
overlayManager.setSecondaryWindowBounds(regions.secondary);
|
|
||||||
overlayManager.setModalWindowBounds(geometry);
|
overlayManager.setModalWindowBounds(geometry);
|
||||||
syncSecondaryOverlayWindowVisibility();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
const buildUpdateVisibleOverlayBoundsMainDepsHandler =
|
||||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
createBuildUpdateVisibleOverlayBoundsMainDepsHandler({
|
||||||
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
setOverlayWindowBounds: (geometry) => applyOverlayRegions(geometry),
|
||||||
});
|
});
|
||||||
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
const updateVisibleOverlayBoundsMainDeps = buildUpdateVisibleOverlayBoundsMainDepsHandler();
|
||||||
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
const updateVisibleOverlayBounds = createUpdateVisibleOverlayBoundsHandler(
|
||||||
updateVisibleOverlayBoundsMainDeps,
|
updateVisibleOverlayBoundsMainDeps,
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildUpdateInvisibleOverlayBoundsMainDepsHandler =
|
|
||||||
createBuildUpdateInvisibleOverlayBoundsMainDepsHandler({
|
|
||||||
setOverlayWindowBounds: (layer, geometry) => applyOverlayRegions(layer, geometry),
|
|
||||||
});
|
|
||||||
const updateInvisibleOverlayBoundsMainDeps = buildUpdateInvisibleOverlayBoundsMainDepsHandler();
|
|
||||||
const updateInvisibleOverlayBounds = createUpdateInvisibleOverlayBoundsHandler(
|
|
||||||
updateInvisibleOverlayBoundsMainDeps,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buildEnsureOverlayWindowLevelMainDepsHandler =
|
const buildEnsureOverlayWindowLevelMainDepsHandler =
|
||||||
createBuildEnsureOverlayWindowLevelMainDepsHandler({
|
createBuildEnsureOverlayWindowLevelMainDepsHandler({
|
||||||
ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow),
|
ensureOverlayWindowLevelCore: (window) => ensureOverlayWindowLevelCore(window as BrowserWindow),
|
||||||
@@ -2331,21 +2363,23 @@ const ensureOverlayWindowLevel = createEnsureOverlayWindowLevelHandler(
|
|||||||
ensureOverlayWindowLevelMainDeps,
|
ensureOverlayWindowLevelMainDeps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function syncPrimaryOverlayWindowLayer(layer: 'visible'): void {
|
||||||
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
|
syncOverlayWindowLayer(mainWindow, layer);
|
||||||
|
}
|
||||||
|
|
||||||
const buildEnforceOverlayLayerOrderMainDepsHandler =
|
const buildEnforceOverlayLayerOrderMainDepsHandler =
|
||||||
createBuildEnforceOverlayLayerOrderMainDepsHandler({
|
createBuildEnforceOverlayLayerOrderMainDepsHandler({
|
||||||
enforceOverlayLayerOrderCore: (params) =>
|
enforceOverlayLayerOrderCore: (params) =>
|
||||||
enforceOverlayLayerOrderCore({
|
enforceOverlayLayerOrderCore({
|
||||||
visibleOverlayVisible: params.visibleOverlayVisible,
|
visibleOverlayVisible: params.visibleOverlayVisible,
|
||||||
invisibleOverlayVisible: params.invisibleOverlayVisible,
|
|
||||||
mainWindow: params.mainWindow as BrowserWindow | null,
|
mainWindow: params.mainWindow as BrowserWindow | null,
|
||||||
invisibleWindow: params.invisibleWindow as BrowserWindow | null,
|
|
||||||
ensureOverlayWindowLevel: (window) =>
|
ensureOverlayWindowLevel: (window) =>
|
||||||
params.ensureOverlayWindowLevel(window as BrowserWindow),
|
params.ensureOverlayWindowLevel(window as BrowserWindow),
|
||||||
}),
|
}),
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window as BrowserWindow),
|
||||||
});
|
});
|
||||||
const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler();
|
const enforceOverlayLayerOrderMainDeps = buildEnforceOverlayLayerOrderMainDepsHandler();
|
||||||
@@ -2361,7 +2395,7 @@ async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
|||||||
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
return yomitanExtensionRuntime.ensureYomitanExtensionLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOverlayWindow(kind: 'visible' | 'invisible' | 'secondary' | 'modal'): BrowserWindow {
|
function createOverlayWindow(kind: 'visible' | 'modal'): BrowserWindow {
|
||||||
return createOverlayWindowHandler(kind);
|
return createOverlayWindowHandler(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2375,25 +2409,9 @@ function createModalWindow(): BrowserWindow {
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSecondaryWindow(): BrowserWindow {
|
|
||||||
const existingWindow = overlayManager.getSecondaryWindow();
|
|
||||||
if (existingWindow && !existingWindow.isDestroyed()) {
|
|
||||||
return existingWindow;
|
|
||||||
}
|
|
||||||
const window = createSecondaryWindowHandler();
|
|
||||||
applyOverlayRegions('visible', getCurrentOverlayGeometry());
|
|
||||||
return window;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMainWindow(): BrowserWindow {
|
function createMainWindow(): BrowserWindow {
|
||||||
const window = createMainWindowHandler();
|
return createMainWindowHandler();
|
||||||
createSecondaryWindow();
|
|
||||||
return window;
|
|
||||||
}
|
}
|
||||||
function createInvisibleWindow(): BrowserWindow {
|
|
||||||
return createInvisibleWindowHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveTrayIconPath(): string | null {
|
function resolveTrayIconPath(): string | null {
|
||||||
return resolveTrayIconPathHandler();
|
return resolveTrayIconPathHandler();
|
||||||
}
|
}
|
||||||
@@ -2412,6 +2430,7 @@ function destroyTray(): void {
|
|||||||
|
|
||||||
function initializeOverlayRuntime(): void {
|
function initializeOverlayRuntime(): void {
|
||||||
initializeOverlayRuntimeHandler();
|
initializeOverlayRuntimeHandler();
|
||||||
|
syncOverlayMpvSubtitleSuppression();
|
||||||
}
|
}
|
||||||
|
|
||||||
function openYomitanSettings(): void {
|
function openYomitanSettings(): void {
|
||||||
@@ -2441,7 +2460,6 @@ const {
|
|||||||
getConfiguredShortcuts: () => getConfiguredShortcutsHandler(),
|
getConfiguredShortcuts: () => getConfiguredShortcutsHandler(),
|
||||||
registerGlobalShortcutsCore,
|
registerGlobalShortcutsCore,
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
isDev,
|
isDev,
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
@@ -2495,8 +2513,7 @@ const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
|
|||||||
cycleSecondarySubModeMainDeps: {
|
cycleSecondarySubModeMainDeps: {
|
||||||
getSecondarySubMode: () => appState.secondarySubMode,
|
getSecondarySubMode: () => appState.secondarySubMode,
|
||||||
setSecondarySubMode: (mode: SecondarySubMode) => {
|
setSecondarySubMode: (mode: SecondarySubMode) => {
|
||||||
appState.secondarySubMode = mode;
|
setSecondarySubMode(mode);
|
||||||
syncSecondaryOverlayWindowVisibility();
|
|
||||||
},
|
},
|
||||||
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
getLastSecondarySubToggleAtMs: () => appState.lastSecondarySubToggleAtMs,
|
||||||
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
setLastSecondarySubToggleAtMs: (timestampMs: number) => {
|
||||||
@@ -2510,6 +2527,15 @@ const cycleSecondarySubMode = createCycleSecondarySubModeRuntimeHandler({
|
|||||||
cycleSecondarySubMode: (deps) => cycleSecondarySubModeCore(deps),
|
cycleSecondarySubMode: (deps) => cycleSecondarySubModeCore(deps),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setSecondarySubMode(mode: SecondarySubMode): void {
|
||||||
|
appState.secondarySubMode = mode;
|
||||||
|
syncOverlayMpvSubtitleSuppression();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCycleSecondarySubMode(): void {
|
||||||
|
cycleSecondarySubMode();
|
||||||
|
}
|
||||||
|
|
||||||
async function triggerSubsyncFromConfig(): Promise<void> {
|
async function triggerSubsyncFromConfig(): Promise<void> {
|
||||||
await subsyncRuntime.triggerFromConfig();
|
await subsyncRuntime.triggerFromConfig();
|
||||||
}
|
}
|
||||||
@@ -2613,9 +2639,7 @@ const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler(
|
|||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
setVisibleOverlayVisible: setVisibleOverlayVisibleHandler,
|
setVisibleOverlayVisible: setVisibleOverlayVisibleHandler,
|
||||||
setInvisibleOverlayVisible: setInvisibleOverlayVisibleHandler,
|
|
||||||
toggleVisibleOverlay: toggleVisibleOverlayHandler,
|
toggleVisibleOverlay: toggleVisibleOverlayHandler,
|
||||||
toggleInvisibleOverlay: toggleInvisibleOverlayHandler,
|
|
||||||
setOverlayVisible: setOverlayVisibleHandler,
|
setOverlayVisible: setOverlayVisibleHandler,
|
||||||
toggleOverlay: toggleOverlayHandler,
|
toggleOverlay: toggleOverlayHandler,
|
||||||
} = createOverlayVisibilityRuntime({
|
} = createOverlayVisibilityRuntime({
|
||||||
@@ -2625,29 +2649,8 @@ const {
|
|||||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||||
},
|
},
|
||||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||||
updateInvisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
|
||||||
syncInvisibleOverlayMousePassthrough: () =>
|
|
||||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
|
||||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
|
||||||
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
|
|
||||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
|
||||||
setMpvSubVisibility: (mpvSubVisible) => {
|
|
||||||
setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setInvisibleOverlayVisibleDeps: {
|
|
||||||
setInvisibleOverlayVisibleCore,
|
|
||||||
setInvisibleOverlayVisibleState: (nextVisible) => {
|
|
||||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
|
||||||
},
|
|
||||||
updateInvisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
|
||||||
syncInvisibleOverlayMousePassthrough: () =>
|
|
||||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
|
||||||
},
|
},
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildHandleOverlayModalClosedMainDepsHandler =
|
const buildHandleOverlayModalClosedMainDepsHandler =
|
||||||
@@ -2707,10 +2710,8 @@ const {
|
|||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
},
|
},
|
||||||
mainDeps: {
|
mainDeps: {
|
||||||
getInvisibleWindow: () => overlayManager.getInvisibleWindow(),
|
|
||||||
getMainWindow: () => overlayManager.getMainWindow(),
|
getMainWindow: () => overlayManager.getMainWindow(),
|
||||||
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisibility: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisibility: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
focusMainWindow: () => {
|
focusMainWindow: () => {
|
||||||
const mainWindow = overlayManager.getMainWindow();
|
const mainWindow = overlayManager.getMainWindow();
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
@@ -2721,13 +2722,15 @@ const {
|
|||||||
onOverlayModalClosed: (modal) => {
|
onOverlayModalClosed: (modal) => {
|
||||||
handleOverlayModalClosed(modal);
|
handleOverlayModalClosed(modal);
|
||||||
},
|
},
|
||||||
|
onOverlayModalOpened: (modal) => {
|
||||||
|
overlayModalRuntime.notifyOverlayModalOpened(modal);
|
||||||
|
},
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
quitApp: () => app.quit(),
|
quitApp: () => app.quit(),
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
tokenizeCurrentSubtitle: () => tokenizeSubtitle(appState.currentSubText),
|
||||||
getCurrentSubtitleRaw: () => appState.currentSubText,
|
getCurrentSubtitleRaw: () => appState.currentSubText,
|
||||||
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
getCurrentSubtitleAss: () => appState.currentSubAssText,
|
||||||
getMpvSubtitleRenderMetrics: () => appState.mpvSubtitleRenderMetrics,
|
|
||||||
getSubtitlePosition: () => loadSubtitlePosition(),
|
getSubtitlePosition: () => loadSubtitlePosition(),
|
||||||
getSubtitleStyle: () => {
|
getSubtitleStyle: () => {
|
||||||
const resolvedConfig = getResolvedConfig();
|
const resolvedConfig = getResolvedConfig();
|
||||||
@@ -2744,9 +2747,6 @@ const {
|
|||||||
reportOverlayContentBounds: (payload: unknown) => {
|
reportOverlayContentBounds: (payload: unknown) => {
|
||||||
overlayContentMeasurementStore.report(payload);
|
overlayContentMeasurementStore.report(payload);
|
||||||
},
|
},
|
||||||
reportHoveredSubtitleToken: (tokenIndex: number | null) => {
|
|
||||||
reportHoveredSubtitleToken(tokenIndex);
|
|
||||||
},
|
|
||||||
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
||||||
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
||||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||||
@@ -2800,9 +2800,7 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||||
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
|
||||||
setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
setVisibleOverlayVisible: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||||
setInvisibleOverlayVisible: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
|
||||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||||
mineSentenceCard: () => mineSentenceCard(),
|
mineSentenceCard: () => mineSentenceCard(),
|
||||||
@@ -2821,7 +2819,7 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
|
processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(),
|
||||||
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
||||||
openYomitanSettings: () => openYomitanSettings(),
|
openYomitanSettings: () => openYomitanSettings(),
|
||||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
cycleSecondarySubMode: () => handleCycleSecondarySubMode(),
|
||||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||||
stopApp: () => app.quit(),
|
stopApp: () => app.quit(),
|
||||||
@@ -2835,40 +2833,29 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
|||||||
const {
|
const {
|
||||||
createOverlayWindow: createOverlayWindowHandler,
|
createOverlayWindow: createOverlayWindowHandler,
|
||||||
createMainWindow: createMainWindowHandler,
|
createMainWindow: createMainWindowHandler,
|
||||||
createInvisibleWindow: createInvisibleWindowHandler,
|
|
||||||
createSecondaryWindow: createSecondaryWindowHandler,
|
|
||||||
createModalWindow: createModalWindowHandler,
|
createModalWindow: createModalWindowHandler,
|
||||||
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
} = createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||||
createOverlayWindowDeps: {
|
createOverlayWindowDeps: {
|
||||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||||
isDev,
|
isDev,
|
||||||
getOverlayDebugVisualizationEnabled: () => appState.overlayDebugVisualizationEnabled,
|
|
||||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||||
isOverlayVisible: (windowKind) =>
|
isOverlayVisible: (windowKind) =>
|
||||||
windowKind === 'visible'
|
windowKind === 'visible'
|
||||||
? overlayManager.getVisibleOverlayVisible()
|
? overlayManager.getVisibleOverlayVisible()
|
||||||
: windowKind === 'invisible'
|
: false,
|
||||||
? overlayManager.getInvisibleOverlayVisible()
|
|
||||||
: false,
|
|
||||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||||
onWindowClosed: (windowKind) => {
|
onWindowClosed: (windowKind) => {
|
||||||
if (windowKind === 'visible') {
|
if (windowKind === 'visible') {
|
||||||
overlayManager.setMainWindow(null);
|
overlayManager.setMainWindow(null);
|
||||||
} else if (windowKind === 'invisible') {
|
|
||||||
overlayManager.setInvisibleWindow(null);
|
|
||||||
} else if (windowKind === 'secondary') {
|
|
||||||
overlayManager.setSecondaryWindow(null);
|
|
||||||
} else {
|
} else {
|
||||||
overlayManager.setModalWindow(null);
|
overlayManager.setModalWindow(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||||
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
|
||||||
setSecondaryWindow: (window) => overlayManager.setSecondaryWindow(window),
|
|
||||||
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
setModalWindow: (window) => overlayManager.setModalWindow(window),
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@@ -2948,24 +2935,17 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
|||||||
appState,
|
appState,
|
||||||
overlayManager: {
|
overlayManager: {
|
||||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
|
||||||
},
|
},
|
||||||
overlayVisibilityRuntime: {
|
overlayVisibilityRuntime: {
|
||||||
updateVisibleOverlayVisibility: () =>
|
updateVisibleOverlayVisibility: () =>
|
||||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||||
updateInvisibleOverlayVisibility: () =>
|
|
||||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
|
||||||
},
|
},
|
||||||
overlayShortcutsRuntime: {
|
overlayShortcutsRuntime: {
|
||||||
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||||
},
|
},
|
||||||
getInitialInvisibleOverlayVisibility: () =>
|
|
||||||
configDerivedRuntime.getInitialInvisibleOverlayVisibility(),
|
|
||||||
createMainWindow: () => createMainWindow(),
|
createMainWindow: () => createMainWindow(),
|
||||||
createInvisibleWindow: () => createInvisibleWindow(),
|
|
||||||
registerGlobalShortcuts: () => registerGlobalShortcuts(),
|
registerGlobalShortcuts: () => registerGlobalShortcuts(),
|
||||||
updateVisibleOverlayBounds: (geometry) => updateVisibleOverlayBounds(geometry),
|
updateVisibleOverlayBounds: (geometry: WindowGeometry) => updateVisibleOverlayBounds(geometry),
|
||||||
updateInvisibleOverlayBounds: (geometry) => updateInvisibleOverlayBounds(geometry),
|
|
||||||
getOverlayWindows: () => getOverlayWindows(),
|
getOverlayWindows: () => getOverlayWindows(),
|
||||||
getResolvedConfig: () => getResolvedConfig(),
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
showDesktopNotification,
|
showDesktopNotification,
|
||||||
@@ -2975,9 +2955,6 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
|
|||||||
initializeOverlayRuntimeBootstrapDeps: {
|
initializeOverlayRuntimeBootstrapDeps: {
|
||||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||||
initializeOverlayRuntimeCore,
|
initializeOverlayRuntimeCore,
|
||||||
setInvisibleOverlayVisible: (visible) => {
|
|
||||||
overlayManager.setInvisibleOverlayVisible(visible);
|
|
||||||
},
|
|
||||||
setOverlayRuntimeInitialized: (initialized) => {
|
setOverlayRuntimeInitialized: (initialized) => {
|
||||||
appState.overlayRuntimeInitialized = initialized;
|
appState.overlayRuntimeInitialized = initialized;
|
||||||
},
|
},
|
||||||
@@ -3035,39 +3012,26 @@ function ensureOverlayWindowsReadyForVisibilityActions(): void {
|
|||||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
const invisibleWindow = overlayManager.getInvisibleWindow();
|
|
||||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) {
|
|
||||||
createInvisibleWindow();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVisibleOverlayVisible(visible: boolean): void {
|
function setVisibleOverlayVisible(visible: boolean): void {
|
||||||
ensureOverlayWindowsReadyForVisibilityActions();
|
ensureOverlayWindowsReadyForVisibilityActions();
|
||||||
setVisibleOverlayVisibleHandler(visible);
|
setVisibleOverlayVisibleHandler(visible);
|
||||||
}
|
syncOverlayMpvSubtitleSuppression();
|
||||||
|
|
||||||
function setInvisibleOverlayVisible(visible: boolean): void {
|
|
||||||
ensureOverlayWindowsReadyForVisibilityActions();
|
|
||||||
setInvisibleOverlayVisibleHandler(visible);
|
|
||||||
if (visible) {
|
|
||||||
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleVisibleOverlay(): void {
|
function toggleVisibleOverlay(): void {
|
||||||
ensureOverlayWindowsReadyForVisibilityActions();
|
ensureOverlayWindowsReadyForVisibilityActions();
|
||||||
toggleVisibleOverlayHandler();
|
toggleVisibleOverlayHandler();
|
||||||
}
|
syncOverlayMpvSubtitleSuppression();
|
||||||
function toggleInvisibleOverlay(): void {
|
|
||||||
ensureOverlayWindowsReadyForVisibilityActions();
|
|
||||||
toggleInvisibleOverlayHandler();
|
|
||||||
}
|
}
|
||||||
function setOverlayVisible(visible: boolean): void {
|
function setOverlayVisible(visible: boolean): void {
|
||||||
setOverlayVisibleHandler(visible);
|
setOverlayVisibleHandler(visible);
|
||||||
|
syncOverlayMpvSubtitleSuppression();
|
||||||
}
|
}
|
||||||
function toggleOverlay(): void {
|
function toggleOverlay(): void {
|
||||||
toggleOverlayHandler();
|
toggleOverlayHandler();
|
||||||
|
syncOverlayMpvSubtitleSuppression();
|
||||||
}
|
}
|
||||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||||
handleOverlayModalClosedHandler(modal);
|
handleOverlayModalClosedHandler(modal);
|
||||||
@@ -3077,11 +3041,6 @@ function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
|||||||
handleMpvCommandFromIpcHandler(command);
|
handleMpvCommandFromIpcHandler(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportHoveredSubtitleToken(tokenIndex: number | null): void {
|
|
||||||
appState.hoveredSubtitleTokenIndex = tokenIndex;
|
|
||||||
applyHoveredTokenOverlay();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promise<SubsyncResult> {
|
async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promise<SubsyncResult> {
|
||||||
return runSubsyncManualFromIpcHandler(request) as Promise<SubsyncResult>;
|
return runSubsyncManualFromIpcHandler(request) as Promise<SubsyncResult>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
|||||||
destroyTray: () => calls.push('destroy-tray'),
|
destroyTray: () => calls.push('destroy-tray'),
|
||||||
stopConfigHotReload: () => calls.push('stop-config'),
|
stopConfigHotReload: () => calls.push('stop-config'),
|
||||||
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'),
|
||||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||||
@@ -33,7 +34,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
assert.equal(calls.length, 21);
|
assert.equal(calls.length, 22);
|
||||||
assert.equal(calls[0], 'destroy-tray');
|
assert.equal(calls[0], 'destroy-tray');
|
||||||
assert.equal(calls[calls.length - 1], 'stop-discord-presence');
|
assert.equal(calls[calls.length - 1], 'stop-discord-presence');
|
||||||
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
||||||
@@ -58,11 +59,10 @@ test('restore windows on activate recreates windows then syncs visibility', () =
|
|||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const restore = createRestoreWindowsOnActivateHandler({
|
const restore = createRestoreWindowsOnActivateHandler({
|
||||||
createMainWindow: () => calls.push('main'),
|
createMainWindow: () => calls.push('main'),
|
||||||
createInvisibleWindow: () => calls.push('invisible'),
|
|
||||||
updateVisibleOverlayVisibility: () => calls.push('visible-sync'),
|
updateVisibleOverlayVisibility: () => calls.push('visible-sync'),
|
||||||
updateInvisibleOverlayVisibility: () => calls.push('invisible-sync'),
|
syncOverlayMpvSubtitleSuppression: () => calls.push('mpv-sync'),
|
||||||
});
|
});
|
||||||
|
|
||||||
restore();
|
restore();
|
||||||
assert.deepEqual(calls, ['main', 'invisible', 'visible-sync', 'invisible-sync']);
|
assert.deepEqual(calls, ['main', 'visible-sync', 'mpv-sync']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
|||||||
destroyTray: () => void;
|
destroyTray: () => void;
|
||||||
stopConfigHotReload: () => void;
|
stopConfigHotReload: () => void;
|
||||||
restorePreviousSecondarySubVisibility: () => void;
|
restorePreviousSecondarySubVisibility: () => void;
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => void;
|
||||||
unregisterAllGlobalShortcuts: () => void;
|
unregisterAllGlobalShortcuts: () => void;
|
||||||
stopSubtitleWebsocket: () => void;
|
stopSubtitleWebsocket: () => void;
|
||||||
stopTexthookerService: () => void;
|
stopTexthookerService: () => void;
|
||||||
@@ -25,6 +26,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
|||||||
deps.destroyTray();
|
deps.destroyTray();
|
||||||
deps.stopConfigHotReload();
|
deps.stopConfigHotReload();
|
||||||
deps.restorePreviousSecondarySubVisibility();
|
deps.restorePreviousSecondarySubVisibility();
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay();
|
||||||
deps.unregisterAllGlobalShortcuts();
|
deps.unregisterAllGlobalShortcuts();
|
||||||
deps.stopSubtitleWebsocket();
|
deps.stopSubtitleWebsocket();
|
||||||
deps.stopTexthookerService();
|
deps.stopTexthookerService();
|
||||||
@@ -55,14 +57,12 @@ export function createShouldRestoreWindowsOnActivateHandler(deps: {
|
|||||||
|
|
||||||
export function createRestoreWindowsOnActivateHandler(deps: {
|
export function createRestoreWindowsOnActivateHandler(deps: {
|
||||||
createMainWindow: () => void;
|
createMainWindow: () => void;
|
||||||
createInvisibleWindow: () => void;
|
|
||||||
updateVisibleOverlayVisibility: () => void;
|
updateVisibleOverlayVisibility: () => void;
|
||||||
updateInvisibleOverlayVisibility: () => void;
|
syncOverlayMpvSubtitleSuppression: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (): void => {
|
return (): void => {
|
||||||
deps.createMainWindow();
|
deps.createMainWindow();
|
||||||
deps.createInvisibleWindow();
|
|
||||||
deps.updateVisibleOverlayVisibility();
|
deps.updateVisibleOverlayVisibility();
|
||||||
deps.updateInvisibleOverlayVisibility();
|
deps.syncOverlayMpvSubtitleSuppression();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
|
|||||||
destroyTray: () => calls.push('destroy-tray'),
|
destroyTray: () => calls.push('destroy-tray'),
|
||||||
stopConfigHotReload: () => calls.push('stop-config'),
|
stopConfigHotReload: () => calls.push('stop-config'),
|
||||||
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'),
|
||||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||||
@@ -72,6 +73,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
|
|||||||
destroyTray: () => {},
|
destroyTray: () => {},
|
||||||
stopConfigHotReload: () => {},
|
stopConfigHotReload: () => {},
|
||||||
restorePreviousSecondarySubVisibility: () => {},
|
restorePreviousSecondarySubVisibility: () => {},
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => {},
|
||||||
unregisterAllGlobalShortcuts: () => {},
|
unregisterAllGlobalShortcuts: () => {},
|
||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
destroyTray: () => void;
|
destroyTray: () => void;
|
||||||
stopConfigHotReload: () => void;
|
stopConfigHotReload: () => void;
|
||||||
restorePreviousSecondarySubVisibility: () => void;
|
restorePreviousSecondarySubVisibility: () => void;
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => void;
|
||||||
unregisterAllGlobalShortcuts: () => void;
|
unregisterAllGlobalShortcuts: () => void;
|
||||||
stopSubtitleWebsocket: () => void;
|
stopSubtitleWebsocket: () => void;
|
||||||
stopTexthookerService: () => void;
|
stopTexthookerService: () => void;
|
||||||
@@ -51,6 +52,8 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
|||||||
destroyTray: () => deps.destroyTray(),
|
destroyTray: () => deps.destroyTray(),
|
||||||
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
||||||
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () =>
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay(),
|
||||||
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
||||||
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
||||||
stopTexthookerService: () => deps.stopTexthookerService(),
|
stopTexthookerService: () => deps.stopTexthookerService(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
|||||||
destroyTray: () => {},
|
destroyTray: () => {},
|
||||||
stopConfigHotReload: () => {},
|
stopConfigHotReload: () => {},
|
||||||
restorePreviousSecondarySubVisibility: () => {},
|
restorePreviousSecondarySubVisibility: () => {},
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => {},
|
||||||
unregisterAllGlobalShortcuts: () => {},
|
unregisterAllGlobalShortcuts: () => {},
|
||||||
stopSubtitleWebsocket: () => {},
|
stopSubtitleWebsocket: () => {},
|
||||||
stopTexthookerService: () => {},
|
stopTexthookerService: () => {},
|
||||||
@@ -43,9 +44,8 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
|||||||
},
|
},
|
||||||
restoreWindowsOnActivateMainDeps: {
|
restoreWindowsOnActivateMainDeps: {
|
||||||
createMainWindow: () => {},
|
createMainWindow: () => {},
|
||||||
createInvisibleWindow: () => {},
|
|
||||||
updateVisibleOverlayVisibility: () => {},
|
updateVisibleOverlayVisibility: () => {},
|
||||||
updateInvisibleOverlayVisibility: () => {},
|
syncOverlayMpvSubtitleSuppression: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function createHandleMpvSecondarySubtitleChangeHandler(deps: {
|
|||||||
export function createHandleMpvMediaPathChangeHandler(deps: {
|
export function createHandleMpvMediaPathChangeHandler(deps: {
|
||||||
updateCurrentMediaPath: (path: string) => void;
|
updateCurrentMediaPath: (path: string) => void;
|
||||||
reportJellyfinRemoteStopped: () => void;
|
reportJellyfinRemoteStopped: () => void;
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => void;
|
||||||
getCurrentAnilistMediaKey: () => string | null;
|
getCurrentAnilistMediaKey: () => string | null;
|
||||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||||
@@ -44,6 +45,7 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
|||||||
deps.updateCurrentMediaPath(path);
|
deps.updateCurrentMediaPath(path);
|
||||||
if (!path) {
|
if (!path) {
|
||||||
deps.reportJellyfinRemoteStopped();
|
deps.reportJellyfinRemoteStopped();
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay();
|
||||||
}
|
}
|
||||||
const mediaKey = deps.getCurrentAnilistMediaKey();
|
const mediaKey = deps.getCurrentAnilistMediaKey();
|
||||||
deps.resetAnilistMediaTracking(mediaKey);
|
deps.resetAnilistMediaTracking(mediaKey);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
|
|
||||||
const bind = createBindMpvMainEventHandlersHandler({
|
const bind = createBindMpvMainEventHandlersHandler({
|
||||||
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||||
hasInitialJellyfinPlayArg: () => false,
|
hasInitialJellyfinPlayArg: () => false,
|
||||||
isOverlayRuntimeInitialized: () => false,
|
isOverlayRuntimeInitialized: () => false,
|
||||||
isQuitOnDisconnectArmed: () => false,
|
isQuitOnDisconnectArmed: () => false,
|
||||||
@@ -35,6 +36,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`),
|
broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`),
|
||||||
|
|
||||||
updateCurrentMediaPath: (path) => calls.push(`media-path:${path}`),
|
updateCurrentMediaPath: (path) => calls.push(`media-path:${path}`),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'),
|
||||||
getCurrentAnilistMediaKey: () => 'media-key',
|
getCurrentAnilistMediaKey: () => 'media-key',
|
||||||
resetAnilistMediaTracking: (key) => calls.push(`reset-media:${String(key)}`),
|
resetAnilistMediaTracking: (key) => calls.push(`reset-media:${String(key)}`),
|
||||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
@@ -62,6 +64,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
handlers.get('subtitle-change')?.({ text: 'line' });
|
handlers.get('subtitle-change')?.({ text: 'line' });
|
||||||
|
handlers.get('media-path-change')?.({ path: '' });
|
||||||
handlers.get('media-title-change')?.({ title: 'Episode 1' });
|
handlers.get('media-title-change')?.({ title: 'Episode 1' });
|
||||||
handlers.get('time-pos-change')?.({ time: 2.5 });
|
handlers.get('time-pos-change')?.({ time: 2.5 });
|
||||||
handlers.get('pause-change')?.({ paused: true });
|
handlers.get('pause-change')?.({ paused: true });
|
||||||
@@ -70,6 +73,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
assert.ok(calls.includes('broadcast-sub:line'));
|
assert.ok(calls.includes('broadcast-sub:line'));
|
||||||
assert.ok(calls.includes('subtitle-change:line'));
|
assert.ok(calls.includes('subtitle-change:line'));
|
||||||
assert.ok(calls.includes('media-title:Episode 1'));
|
assert.ok(calls.includes('media-title:Episode 1'));
|
||||||
|
assert.ok(calls.includes('restore-mpv-sub'));
|
||||||
assert.ok(calls.includes('reset-guess-state'));
|
assert.ok(calls.includes('reset-guess-state'));
|
||||||
assert.ok(calls.includes('notify-title:Episode 1'));
|
assert.ok(calls.includes('notify-title:Episode 1'));
|
||||||
assert.ok(calls.includes('progress:normal'));
|
assert.ok(calls.includes('progress:normal'));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type MpvEventClient = Parameters<ReturnType<typeof createBindMpvClientEventHandl
|
|||||||
|
|
||||||
export function createBindMpvMainEventHandlersHandler(deps: {
|
export function createBindMpvMainEventHandlersHandler(deps: {
|
||||||
reportJellyfinRemoteStopped: () => void;
|
reportJellyfinRemoteStopped: () => void;
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => void;
|
||||||
hasInitialJellyfinPlayArg: () => boolean;
|
hasInitialJellyfinPlayArg: () => boolean;
|
||||||
isOverlayRuntimeInitialized: () => boolean;
|
isOverlayRuntimeInitialized: () => boolean;
|
||||||
isQuitOnDisconnectArmed: () => boolean;
|
isQuitOnDisconnectArmed: () => boolean;
|
||||||
@@ -42,6 +43,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
broadcastSecondarySubtitle: (text: string) => void;
|
broadcastSecondarySubtitle: (text: string) => void;
|
||||||
|
|
||||||
updateCurrentMediaPath: (path: string) => void;
|
updateCurrentMediaPath: (path: string) => void;
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => void;
|
||||||
getCurrentAnilistMediaKey: () => string | null;
|
getCurrentAnilistMediaKey: () => string | null;
|
||||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||||
@@ -63,6 +65,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
const handleMpvConnectionChange = createHandleMpvConnectionChangeHandler({
|
const handleMpvConnectionChange = createHandleMpvConnectionChangeHandler({
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||||
hasInitialJellyfinPlayArg: () => deps.hasInitialJellyfinPlayArg(),
|
hasInitialJellyfinPlayArg: () => deps.hasInitialJellyfinPlayArg(),
|
||||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||||
isQuitOnDisconnectArmed: () => deps.isQuitOnDisconnectArmed(),
|
isQuitOnDisconnectArmed: () => deps.isQuitOnDisconnectArmed(),
|
||||||
@@ -94,6 +97,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
|
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
|
||||||
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
|
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () =>
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay(),
|
||||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||||
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
|
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
|
||||||
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
},
|
},
|
||||||
quitApp: () => calls.push('quit'),
|
quitApp: () => calls.push('quit'),
|
||||||
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||||
maybeRunAnilistPostWatchUpdate: async () => {
|
maybeRunAnilistPostWatchUpdate: async () => {
|
||||||
calls.push('anilist-post-watch');
|
calls.push('anilist-post-watch');
|
||||||
},
|
},
|
||||||
@@ -40,6 +41,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
calls.push(`broadcast:${channel}:${String(payload)}`),
|
calls.push(`broadcast:${channel}:${String(payload)}`),
|
||||||
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
|
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
|
||||||
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => calls.push('restore-mpv-sub'),
|
||||||
getCurrentAnilistMediaKey: () => 'media-key',
|
getCurrentAnilistMediaKey: () => 'media-key',
|
||||||
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`),
|
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`),
|
||||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
@@ -59,6 +61,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
deps.scheduleQuitCheck(() => calls.push('scheduled-callback'));
|
deps.scheduleQuitCheck(() => calls.push('scheduled-callback'));
|
||||||
deps.quitApp();
|
deps.quitApp();
|
||||||
deps.reportJellyfinRemoteStopped();
|
deps.reportJellyfinRemoteStopped();
|
||||||
|
deps.syncOverlayMpvSubtitleSuppression();
|
||||||
deps.recordImmersionSubtitleLine('x', 0, 1);
|
deps.recordImmersionSubtitleLine('x', 0, 1);
|
||||||
assert.equal(deps.hasSubtitleTimingTracker(), true);
|
assert.equal(deps.hasSubtitleTimingTracker(), true);
|
||||||
deps.recordSubtitleTiming('y', 0, 1);
|
deps.recordSubtitleTiming('y', 0, 1);
|
||||||
@@ -72,6 +75,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
deps.broadcastSubtitleAss('ass');
|
deps.broadcastSubtitleAss('ass');
|
||||||
deps.broadcastSecondarySubtitle('sec');
|
deps.broadcastSecondarySubtitle('sec');
|
||||||
deps.updateCurrentMediaPath('/tmp/video');
|
deps.updateCurrentMediaPath('/tmp/video');
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay();
|
||||||
assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key');
|
assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key');
|
||||||
deps.resetAnilistMediaTracking('media-key');
|
deps.resetAnilistMediaTracking('media-key');
|
||||||
deps.maybeProbeAnilistDuration('media-key');
|
deps.maybeProbeAnilistDuration('media-key');
|
||||||
@@ -91,8 +95,10 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
assert.equal(appState.playbackPaused, true);
|
assert.equal(appState.playbackPaused, true);
|
||||||
assert.equal(appState.previousSecondarySubVisibility, true);
|
assert.equal(appState.previousSecondarySubVisibility, true);
|
||||||
assert.ok(calls.includes('remote-stopped'));
|
assert.ok(calls.includes('remote-stopped'));
|
||||||
|
assert.ok(calls.includes('sync-overlay-mpv-sub'));
|
||||||
assert.ok(calls.includes('anilist-post-watch'));
|
assert.ok(calls.includes('anilist-post-watch'));
|
||||||
assert.ok(calls.includes('sync-immersion'));
|
assert.ok(calls.includes('sync-immersion'));
|
||||||
assert.ok(calls.includes('metrics'));
|
assert.ok(calls.includes('metrics'));
|
||||||
assert.ok(calls.includes('presence-refresh'));
|
assert.ok(calls.includes('presence-refresh'));
|
||||||
|
assert.ok(calls.includes('restore-mpv-sub'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
scheduleQuitCheck: (callback: () => void) => void;
|
scheduleQuitCheck: (callback: () => void) => void;
|
||||||
quitApp: () => void;
|
quitApp: () => void;
|
||||||
reportJellyfinRemoteStopped: () => void;
|
reportJellyfinRemoteStopped: () => void;
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => void;
|
||||||
maybeRunAnilistPostWatchUpdate: () => Promise<void>;
|
maybeRunAnilistPostWatchUpdate: () => Promise<void>;
|
||||||
logSubtitleTimingError: (message: string, error: unknown) => void;
|
logSubtitleTimingError: (message: string, error: unknown) => void;
|
||||||
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||||
onSubtitleChange: (text: string) => void;
|
onSubtitleChange: (text: string) => void;
|
||||||
updateCurrentMediaPath: (path: string) => void;
|
updateCurrentMediaPath: (path: string) => void;
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () => void;
|
||||||
getCurrentAnilistMediaKey: () => string | null;
|
getCurrentAnilistMediaKey: () => string | null;
|
||||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||||
@@ -39,6 +41,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||||
hasInitialJellyfinPlayArg: () => Boolean(deps.appState.initialArgs?.jellyfinPlay),
|
hasInitialJellyfinPlayArg: () => Boolean(deps.appState.initialArgs?.jellyfinPlay),
|
||||||
isOverlayRuntimeInitialized: () => deps.appState.overlayRuntimeInitialized,
|
isOverlayRuntimeInitialized: () => deps.appState.overlayRuntimeInitialized,
|
||||||
isQuitOnDisconnectArmed: () => deps.getQuitOnDisconnectArmed(),
|
isQuitOnDisconnectArmed: () => deps.getQuitOnDisconnectArmed(),
|
||||||
@@ -68,6 +71,8 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
broadcastSecondarySubtitle: (text: string) =>
|
broadcastSecondarySubtitle: (text: string) =>
|
||||||
deps.broadcastToOverlayWindows('secondary-subtitle:set', text),
|
deps.broadcastToOverlayWindows('secondary-subtitle:set', text),
|
||||||
updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path),
|
updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path),
|
||||||
|
restoreMpvSubVisibilityForInvisibleOverlay: () =>
|
||||||
|
deps.restoreMpvSubVisibilityForInvisibleOverlay(),
|
||||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||||
resetAnilistMediaTracking: (mediaKey: string | null) =>
|
resetAnilistMediaTracking: (mediaKey: string | null) =>
|
||||||
deps.resetAnilistMediaTracking(mediaKey),
|
deps.resetAnilistMediaTracking(mediaKey),
|
||||||
|
|||||||
171
src/main/runtime/overlay-mpv-sub-visibility.test.ts
Normal file
171
src/main/runtime/overlay-mpv-sub-visibility.test.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createEnsureOverlayMpvSubtitlesHiddenHandler,
|
||||||
|
createRestoreOverlayMpvSubtitlesHandler,
|
||||||
|
} from './overlay-mpv-sub-visibility';
|
||||||
|
|
||||||
|
type VisibilityState = {
|
||||||
|
savedSubVisibility: boolean | null;
|
||||||
|
savedSecondarySubVisibility: boolean | null;
|
||||||
|
revision: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('ensure overlay mpv subtitle suppression captures previous visibility then hides subtitles', async () => {
|
||||||
|
const state: VisibilityState = {
|
||||||
|
savedSubVisibility: null,
|
||||||
|
savedSecondarySubVisibility: null,
|
||||||
|
revision: 0,
|
||||||
|
};
|
||||||
|
const calls: boolean[] = [];
|
||||||
|
|
||||||
|
const ensureHidden = createEnsureOverlayMpvSubtitlesHiddenHandler({
|
||||||
|
getMpvClient: () => ({
|
||||||
|
connected: true,
|
||||||
|
requestProperty: async (_name: string) => 'no',
|
||||||
|
}),
|
||||||
|
getSavedSubVisibility: () => state.savedSubVisibility,
|
||||||
|
setSavedSubVisibility: (visible) => {
|
||||||
|
state.savedSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
state.savedSecondarySubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => state.revision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
state.revision = revision;
|
||||||
|
},
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
logWarn: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ensureHidden();
|
||||||
|
|
||||||
|
assert.equal(state.savedSubVisibility, false);
|
||||||
|
assert.equal(state.savedSecondarySubVisibility, false);
|
||||||
|
assert.equal(state.revision, 1);
|
||||||
|
assert.deepEqual(calls, [false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore overlay mpv subtitle suppression restores saved visibility', () => {
|
||||||
|
const state: VisibilityState = {
|
||||||
|
savedSubVisibility: false,
|
||||||
|
savedSecondarySubVisibility: true,
|
||||||
|
revision: 4,
|
||||||
|
};
|
||||||
|
const calls: boolean[] = [];
|
||||||
|
|
||||||
|
const restore = createRestoreOverlayMpvSubtitlesHandler({
|
||||||
|
getSavedSubVisibility: () => state.savedSubVisibility,
|
||||||
|
setSavedSubVisibility: (visible) => {
|
||||||
|
state.savedSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
state.savedSecondarySubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => state.revision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
state.revision = revision;
|
||||||
|
},
|
||||||
|
isMpvConnected: () => true,
|
||||||
|
shouldKeepSuppressedFromVisibleOverlayBinding: () => false,
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
restore();
|
||||||
|
|
||||||
|
assert.equal(state.savedSubVisibility, null);
|
||||||
|
assert.equal(state.savedSecondarySubVisibility, null);
|
||||||
|
assert.equal(state.revision, 5);
|
||||||
|
assert.deepEqual(calls, [false, true]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore keeps mpv subtitles hidden when visible-overlay binding still requires suppression', () => {
|
||||||
|
const state: VisibilityState = {
|
||||||
|
savedSubVisibility: true,
|
||||||
|
savedSecondarySubVisibility: true,
|
||||||
|
revision: 9,
|
||||||
|
};
|
||||||
|
const calls: boolean[] = [];
|
||||||
|
|
||||||
|
const restore = createRestoreOverlayMpvSubtitlesHandler({
|
||||||
|
getSavedSubVisibility: () => state.savedSubVisibility,
|
||||||
|
setSavedSubVisibility: (visible) => {
|
||||||
|
state.savedSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
state.savedSecondarySubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => state.revision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
state.revision = revision;
|
||||||
|
},
|
||||||
|
isMpvConnected: () => true,
|
||||||
|
shouldKeepSuppressedFromVisibleOverlayBinding: () => true,
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
restore();
|
||||||
|
|
||||||
|
assert.equal(state.savedSubVisibility, true);
|
||||||
|
assert.equal(state.savedSecondarySubVisibility, true);
|
||||||
|
assert.equal(state.revision, 10);
|
||||||
|
assert.deepEqual(calls, [false, false]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restore defers mpv subtitle restore while mpv is disconnected', () => {
|
||||||
|
const state: VisibilityState = {
|
||||||
|
savedSubVisibility: true,
|
||||||
|
savedSecondarySubVisibility: false,
|
||||||
|
revision: 2,
|
||||||
|
};
|
||||||
|
const calls: boolean[] = [];
|
||||||
|
|
||||||
|
const restore = createRestoreOverlayMpvSubtitlesHandler({
|
||||||
|
getSavedSubVisibility: () => state.savedSubVisibility,
|
||||||
|
setSavedSubVisibility: (visible) => {
|
||||||
|
state.savedSubVisibility = visible;
|
||||||
|
},
|
||||||
|
getSavedSecondarySubVisibility: () => state.savedSecondarySubVisibility,
|
||||||
|
setSavedSecondarySubVisibility: (visible) => {
|
||||||
|
state.savedSecondarySubVisibility = visible;
|
||||||
|
},
|
||||||
|
getRevision: () => state.revision,
|
||||||
|
setRevision: (revision) => {
|
||||||
|
state.revision = revision;
|
||||||
|
},
|
||||||
|
isMpvConnected: () => false,
|
||||||
|
shouldKeepSuppressedFromVisibleOverlayBinding: () => false,
|
||||||
|
setMpvSubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
setMpvSecondarySubVisibility: (visible) => {
|
||||||
|
calls.push(visible);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
restore();
|
||||||
|
|
||||||
|
assert.equal(state.savedSubVisibility, true);
|
||||||
|
assert.equal(state.revision, 3);
|
||||||
|
assert.deepEqual(calls, []);
|
||||||
|
});
|
||||||
147
src/main/runtime/overlay-mpv-sub-visibility.ts
Normal file
147
src/main/runtime/overlay-mpv-sub-visibility.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
type MpvVisibilityClient = {
|
||||||
|
connected: boolean;
|
||||||
|
requestProperty: (name: string) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RestoreOptions = {
|
||||||
|
respectVisibleOverlayBinding?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseSubVisibility(value: unknown): boolean {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (normalized === 'no' || normalized === 'false' || normalized === '0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (normalized === 'yes' || normalized === 'true' || normalized === '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return value !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEnsureOverlayMpvSubtitlesHiddenHandler(deps: {
|
||||||
|
getMpvClient: () => MpvVisibilityClient | null;
|
||||||
|
getSavedSubVisibility: () => boolean | null;
|
||||||
|
setSavedSubVisibility: (visible: boolean | null) => void;
|
||||||
|
getSavedSecondarySubVisibility: () => boolean | null;
|
||||||
|
setSavedSecondarySubVisibility: (visible: boolean | null) => void;
|
||||||
|
getRevision: () => number;
|
||||||
|
setRevision: (revision: number) => void;
|
||||||
|
setMpvSubVisibility: (visible: boolean) => void;
|
||||||
|
setMpvSecondarySubVisibility: (visible: boolean) => void;
|
||||||
|
logWarn: (message: string, error: unknown) => void;
|
||||||
|
}) {
|
||||||
|
return async (): Promise<void> => {
|
||||||
|
const revision = deps.getRevision() + 1;
|
||||||
|
deps.setRevision(revision);
|
||||||
|
|
||||||
|
const mpvClient = deps.getMpvClient();
|
||||||
|
if (!mpvClient || !mpvClient.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deps.getSavedSubVisibility() === null) {
|
||||||
|
try {
|
||||||
|
const currentSubVisibility = await mpvClient.requestProperty('sub-visibility');
|
||||||
|
if (revision !== deps.getRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.setSavedSubVisibility(parseSubVisibility(currentSubVisibility));
|
||||||
|
} catch (error) {
|
||||||
|
if (revision !== deps.getRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.logWarn(
|
||||||
|
'[overlay] Failed to capture mpv sub-visibility; falling back to visible restore',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
deps.setSavedSubVisibility(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deps.getSavedSecondarySubVisibility() === null) {
|
||||||
|
try {
|
||||||
|
const currentSecondarySubVisibility = await mpvClient.requestProperty('secondary-sub-visibility');
|
||||||
|
if (revision !== deps.getRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.setSavedSecondarySubVisibility(parseSubVisibility(currentSecondarySubVisibility));
|
||||||
|
} catch (error) {
|
||||||
|
if (revision !== deps.getRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.logWarn(
|
||||||
|
'[overlay] Failed to capture secondary mpv sub-visibility; falling back to visible restore',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
deps.setSavedSecondarySubVisibility(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revision !== deps.getRevision()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.setMpvSubVisibility(false);
|
||||||
|
deps.setMpvSecondarySubVisibility(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRestoreOverlayMpvSubtitlesHandler(deps: {
|
||||||
|
getSavedSubVisibility: () => boolean | null;
|
||||||
|
setSavedSubVisibility: (visible: boolean | null) => void;
|
||||||
|
getSavedSecondarySubVisibility: () => boolean | null;
|
||||||
|
setSavedSecondarySubVisibility: (visible: boolean | null) => void;
|
||||||
|
getRevision: () => number;
|
||||||
|
setRevision: (revision: number) => void;
|
||||||
|
isMpvConnected: () => boolean;
|
||||||
|
shouldKeepSuppressedFromVisibleOverlayBinding: () => boolean;
|
||||||
|
setMpvSubVisibility: (visible: boolean) => void;
|
||||||
|
setMpvSecondarySubVisibility: (visible: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (options?: RestoreOptions): void => {
|
||||||
|
deps.setRevision(deps.getRevision() + 1);
|
||||||
|
|
||||||
|
const savedVisibility = deps.getSavedSubVisibility();
|
||||||
|
const respectVisibleOverlayBinding = options?.respectVisibleOverlayBinding ?? true;
|
||||||
|
if (
|
||||||
|
respectVisibleOverlayBinding &&
|
||||||
|
deps.shouldKeepSuppressedFromVisibleOverlayBinding()
|
||||||
|
) {
|
||||||
|
deps.setMpvSubVisibility(false);
|
||||||
|
deps.setMpvSecondarySubVisibility(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSecondarySavedVisibility = deps.getSavedSecondarySubVisibility() !== null;
|
||||||
|
|
||||||
|
if (savedVisibility === null && !hasSecondarySavedVisibility) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deps.isMpvConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedVisibility !== null) {
|
||||||
|
deps.setMpvSubVisibility(savedVisibility);
|
||||||
|
}
|
||||||
|
const savedSecondaryVisibility = deps.getSavedSecondarySubVisibility();
|
||||||
|
if (savedSecondaryVisibility !== null) {
|
||||||
|
deps.setMpvSecondarySubVisibility(savedSecondaryVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.setSavedSubVisibility(null);
|
||||||
|
deps.setSavedSecondarySubVisibility(null);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import {
|
import {
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
createBuildCreateModalWindowMainDepsHandler,
|
createBuildCreateModalWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
|
|
||||||
test('overlay window factory main deps builders return mapped handlers', () => {
|
test('overlay window factory main deps builders return mapped handlers', () => {
|
||||||
@@ -13,7 +11,6 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
|||||||
const buildOverlayDeps = createBuildCreateOverlayWindowMainDepsHandler({
|
const buildOverlayDeps = createBuildCreateOverlayWindowMainDepsHandler({
|
||||||
createOverlayWindowCore: (kind) => ({ kind }),
|
createOverlayWindowCore: (kind) => ({ kind }),
|
||||||
isDev: true,
|
isDev: true,
|
||||||
getOverlayDebugVisualizationEnabled: () => false,
|
|
||||||
ensureOverlayWindowLevel: () => calls.push('ensure-level'),
|
ensureOverlayWindowLevel: () => calls.push('ensure-level'),
|
||||||
onRuntimeOptionsChanged: () => calls.push('runtime-options-changed'),
|
onRuntimeOptionsChanged: () => calls.push('runtime-options-changed'),
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
||||||
@@ -24,7 +21,6 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
|||||||
|
|
||||||
const overlayDeps = buildOverlayDeps();
|
const overlayDeps = buildOverlayDeps();
|
||||||
assert.equal(overlayDeps.isDev, true);
|
assert.equal(overlayDeps.isDev, true);
|
||||||
assert.equal(overlayDeps.getOverlayDebugVisualizationEnabled(), false);
|
|
||||||
assert.equal(overlayDeps.isOverlayVisible('visible'), true);
|
assert.equal(overlayDeps.isOverlayVisible('visible'), true);
|
||||||
|
|
||||||
const buildMainDeps = createBuildCreateMainWindowMainDepsHandler({
|
const buildMainDeps = createBuildCreateMainWindowMainDepsHandler({
|
||||||
@@ -34,20 +30,6 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
|||||||
const mainDeps = buildMainDeps();
|
const mainDeps = buildMainDeps();
|
||||||
mainDeps.setMainWindow(null);
|
mainDeps.setMainWindow(null);
|
||||||
|
|
||||||
const buildInvisibleDeps = createBuildCreateInvisibleWindowMainDepsHandler({
|
|
||||||
createOverlayWindow: () => ({ id: 'invisible' }),
|
|
||||||
setInvisibleWindow: () => calls.push('set-invisible'),
|
|
||||||
});
|
|
||||||
const invisibleDeps = buildInvisibleDeps();
|
|
||||||
invisibleDeps.setInvisibleWindow(null);
|
|
||||||
|
|
||||||
const buildSecondaryDeps = createBuildCreateSecondaryWindowMainDepsHandler({
|
|
||||||
createOverlayWindow: () => ({ id: 'secondary' }),
|
|
||||||
setSecondaryWindow: () => calls.push('set-secondary'),
|
|
||||||
});
|
|
||||||
const secondaryDeps = buildSecondaryDeps();
|
|
||||||
secondaryDeps.setSecondaryWindow(null);
|
|
||||||
|
|
||||||
const buildModalDeps = createBuildCreateModalWindowMainDepsHandler({
|
const buildModalDeps = createBuildCreateModalWindowMainDepsHandler({
|
||||||
createOverlayWindow: () => ({ id: 'modal' }),
|
createOverlayWindow: () => ({ id: 'modal' }),
|
||||||
setModalWindow: () => calls.push('set-modal'),
|
setModalWindow: () => calls.push('set-modal'),
|
||||||
@@ -55,5 +37,5 @@ test('overlay window factory main deps builders return mapped handlers', () => {
|
|||||||
const modalDeps = buildModalDeps();
|
const modalDeps = buildModalDeps();
|
||||||
modalDeps.setModalWindow(null);
|
modalDeps.setModalWindow(null);
|
||||||
|
|
||||||
assert.deepEqual(calls, ['set-main', 'set-invisible', 'set-secondary', 'set-modal']);
|
assert.deepEqual(calls, ['set-main', 'set-modal']);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
kind: 'visible' | 'invisible' | 'secondary' | 'modal',
|
kind: 'visible' | 'modal',
|
||||||
options: {
|
options: {
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
overlayDebugVisualizationEnabled: boolean;
|
|
||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'modal') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
|
onWindowClosed: (windowKind: 'visible' | 'modal') => void;
|
||||||
},
|
},
|
||||||
) => TWindow;
|
) => TWindow;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
getOverlayDebugVisualizationEnabled: () => boolean;
|
|
||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
isOverlayVisible: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => boolean;
|
isOverlayVisible: (windowKind: 'visible' | 'modal') => boolean;
|
||||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||||
onWindowClosed: (windowKind: 'visible' | 'invisible' | 'secondary' | 'modal') => void;
|
onWindowClosed: (windowKind: 'visible' | 'modal') => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
createOverlayWindowCore: deps.createOverlayWindowCore,
|
createOverlayWindowCore: deps.createOverlayWindowCore,
|
||||||
isDev: deps.isDev,
|
isDev: deps.isDev,
|
||||||
getOverlayDebugVisualizationEnabled: deps.getOverlayDebugVisualizationEnabled,
|
|
||||||
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
||||||
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
||||||
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
||||||
@@ -35,7 +32,7 @@ export function createBuildCreateOverlayWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'modal') => TWindow;
|
||||||
setMainWindow: (window: TWindow | null) => void;
|
setMainWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -44,28 +41,8 @@ export function createBuildCreateMainWindowMainDepsHandler<TWindow>(deps: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBuildCreateInvisibleWindowMainDepsHandler<TWindow>(deps: {
|
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
|
||||||
}) {
|
|
||||||
return () => ({
|
|
||||||
createOverlayWindow: deps.createOverlayWindow,
|
|
||||||
setInvisibleWindow: deps.setInvisibleWindow,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBuildCreateSecondaryWindowMainDepsHandler<TWindow>(deps: {
|
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
|
||||||
setSecondaryWindow: (window: TWindow | null) => void;
|
|
||||||
}) {
|
|
||||||
return () => ({
|
|
||||||
createOverlayWindow: deps.createOverlayWindow,
|
|
||||||
setSecondaryWindow: deps.setSecondaryWindow,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
|
export function createBuildCreateModalWindowMainDepsHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: 'visible' | 'invisible' | 'secondary' | 'modal') => TWindow;
|
createOverlayWindow: (kind: 'visible' | 'modal') => TWindow;
|
||||||
setModalWindow: (window: TWindow | null) => void;
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
return () => ({
|
return () => ({
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
createCreateInvisibleWindowHandler,
|
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
createCreateModalWindowHandler,
|
createCreateModalWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
createCreateSecondaryWindowHandler,
|
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
|
|
||||||
test('create overlay window handler forwards options and kind', () => {
|
test('create overlay window handler forwards options and kind', () => {
|
||||||
@@ -15,16 +13,14 @@ test('create overlay window handler forwards options and kind', () => {
|
|||||||
createOverlayWindowCore: (kind, options) => {
|
createOverlayWindowCore: (kind, options) => {
|
||||||
calls.push(`kind:${kind}`);
|
calls.push(`kind:${kind}`);
|
||||||
assert.equal(options.isDev, true);
|
assert.equal(options.isDev, true);
|
||||||
assert.equal(options.overlayDebugVisualizationEnabled, false);
|
|
||||||
assert.equal(options.isOverlayVisible('visible'), true);
|
assert.equal(options.isOverlayVisible('visible'), true);
|
||||||
assert.equal(options.isOverlayVisible('invisible'), false);
|
assert.equal(options.isOverlayVisible('modal'), false);
|
||||||
options.onRuntimeOptionsChanged();
|
options.onRuntimeOptionsChanged();
|
||||||
options.setOverlayDebugVisualizationEnabled(true);
|
options.setOverlayDebugVisualizationEnabled(true);
|
||||||
options.onWindowClosed(kind);
|
options.onWindowClosed(kind);
|
||||||
return window;
|
return window;
|
||||||
},
|
},
|
||||||
isDev: true,
|
isDev: true,
|
||||||
getOverlayDebugVisualizationEnabled: () => false,
|
|
||||||
ensureOverlayWindowLevel: () => {},
|
ensureOverlayWindowLevel: () => {},
|
||||||
onRuntimeOptionsChanged: () => calls.push('runtime-options'),
|
onRuntimeOptionsChanged: () => calls.push('runtime-options'),
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
||||||
@@ -52,36 +48,6 @@ test('create main window handler stores visible window', () => {
|
|||||||
assert.deepEqual(calls, ['create:visible', 'set:visible']);
|
assert.deepEqual(calls, ['create:visible', 'set:visible']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('create invisible window handler stores invisible window', () => {
|
|
||||||
const calls: string[] = [];
|
|
||||||
const invisibleWindow = { id: 'invisible' };
|
|
||||||
const createInvisibleWindow = createCreateInvisibleWindowHandler({
|
|
||||||
createOverlayWindow: (kind) => {
|
|
||||||
calls.push(`create:${kind}`);
|
|
||||||
return invisibleWindow;
|
|
||||||
},
|
|
||||||
setInvisibleWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(createInvisibleWindow(), invisibleWindow);
|
|
||||||
assert.deepEqual(calls, ['create:invisible', 'set:invisible']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('create secondary window handler stores secondary window', () => {
|
|
||||||
const calls: string[] = [];
|
|
||||||
const secondaryWindow = { id: 'secondary' };
|
|
||||||
const createSecondaryWindow = createCreateSecondaryWindowHandler({
|
|
||||||
createOverlayWindow: (kind) => {
|
|
||||||
calls.push(`create:${kind}`);
|
|
||||||
return secondaryWindow;
|
|
||||||
},
|
|
||||||
setSecondaryWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(createSecondaryWindow(), secondaryWindow);
|
|
||||||
assert.deepEqual(calls, ['create:secondary', 'set:secondary']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('create modal window handler stores modal window', () => {
|
test('create modal window handler stores modal window', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const modalWindow = { id: 'modal' };
|
const modalWindow = { id: 'modal' };
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
type OverlayWindowKind = 'visible' | 'invisible' | 'secondary' | 'modal';
|
type OverlayWindowKind = 'visible' | 'modal';
|
||||||
|
|
||||||
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||||
createOverlayWindowCore: (
|
createOverlayWindowCore: (
|
||||||
kind: OverlayWindowKind,
|
kind: OverlayWindowKind,
|
||||||
options: {
|
options: {
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
overlayDebugVisualizationEnabled: boolean;
|
|
||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
@@ -15,7 +14,6 @@ export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
|||||||
},
|
},
|
||||||
) => TWindow;
|
) => TWindow;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
getOverlayDebugVisualizationEnabled: () => boolean;
|
|
||||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||||
onRuntimeOptionsChanged: () => void;
|
onRuntimeOptionsChanged: () => void;
|
||||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||||
@@ -26,7 +24,6 @@ export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
|||||||
return (kind: OverlayWindowKind): TWindow => {
|
return (kind: OverlayWindowKind): TWindow => {
|
||||||
return deps.createOverlayWindowCore(kind, {
|
return deps.createOverlayWindowCore(kind, {
|
||||||
isDev: deps.isDev,
|
isDev: deps.isDev,
|
||||||
overlayDebugVisualizationEnabled: deps.getOverlayDebugVisualizationEnabled(),
|
|
||||||
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
||||||
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
||||||
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
||||||
@@ -48,28 +45,6 @@ export function createCreateMainWindowHandler<TWindow>(deps: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createCreateInvisibleWindowHandler<TWindow>(deps: {
|
|
||||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
|
||||||
}) {
|
|
||||||
return (): TWindow => {
|
|
||||||
const window = deps.createOverlayWindow('invisible');
|
|
||||||
deps.setInvisibleWindow(window);
|
|
||||||
return window;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCreateSecondaryWindowHandler<TWindow>(deps: {
|
|
||||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
|
||||||
setSecondaryWindow: (window: TWindow | null) => void;
|
|
||||||
}) {
|
|
||||||
return (): TWindow => {
|
|
||||||
const window = deps.createOverlayWindow('secondary');
|
|
||||||
deps.setSecondaryWindow(window);
|
|
||||||
return window;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCreateModalWindowHandler<TWindow>(deps: {
|
export function createCreateModalWindowHandler<TWindow>(deps: {
|
||||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||||
setModalWindow: (window: TWindow | null) => void;
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { createOverlayWindowRuntimeHandlers } from './overlay-window-runtime-handlers';
|
import { createOverlayWindowRuntimeHandlers } from './overlay-window-runtime-handlers';
|
||||||
|
|
||||||
test('overlay window runtime handlers compose create/main/invisible handlers', () => {
|
test('overlay window runtime handlers compose create/main/modal handlers', () => {
|
||||||
let mainWindow: { kind: string } | null = null;
|
let mainWindow: { kind: string } | null = null;
|
||||||
let invisibleWindow: { kind: string } | null = null;
|
|
||||||
let secondaryWindow: { kind: string } | null = null;
|
|
||||||
let modalWindow: { kind: string } | null = null;
|
let modalWindow: { kind: string } | null = null;
|
||||||
let debugEnabled = false;
|
let debugEnabled = false;
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
@@ -14,7 +12,6 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
|||||||
createOverlayWindowDeps: {
|
createOverlayWindowDeps: {
|
||||||
createOverlayWindowCore: (kind) => ({ kind }),
|
createOverlayWindowCore: (kind) => ({ kind }),
|
||||||
isDev: true,
|
isDev: true,
|
||||||
getOverlayDebugVisualizationEnabled: () => debugEnabled,
|
|
||||||
ensureOverlayWindowLevel: () => calls.push('ensure-level'),
|
ensureOverlayWindowLevel: () => calls.push('ensure-level'),
|
||||||
onRuntimeOptionsChanged: () => calls.push('runtime-options-changed'),
|
onRuntimeOptionsChanged: () => calls.push('runtime-options-changed'),
|
||||||
setOverlayDebugVisualizationEnabled: (enabled) => {
|
setOverlayDebugVisualizationEnabled: (enabled) => {
|
||||||
@@ -27,29 +24,17 @@ test('overlay window runtime handlers compose create/main/invisible handlers', (
|
|||||||
setMainWindow: (window) => {
|
setMainWindow: (window) => {
|
||||||
mainWindow = window;
|
mainWindow = window;
|
||||||
},
|
},
|
||||||
setInvisibleWindow: (window) => {
|
|
||||||
invisibleWindow = window;
|
|
||||||
},
|
|
||||||
setSecondaryWindow: (window) => {
|
|
||||||
secondaryWindow = window;
|
|
||||||
},
|
|
||||||
setModalWindow: (window) => {
|
setModalWindow: (window) => {
|
||||||
modalWindow = window;
|
modalWindow = window;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
assert.deepEqual(runtime.createOverlayWindow('visible'), { kind: 'visible' });
|
||||||
assert.deepEqual(runtime.createOverlayWindow('invisible'), { kind: 'invisible' });
|
assert.deepEqual(runtime.createOverlayWindow('modal'), { kind: 'modal' });
|
||||||
assert.deepEqual(runtime.createOverlayWindow('secondary'), { kind: 'secondary' });
|
|
||||||
|
|
||||||
assert.deepEqual(runtime.createMainWindow(), { kind: 'visible' });
|
assert.deepEqual(runtime.createMainWindow(), { kind: 'visible' });
|
||||||
assert.deepEqual(mainWindow, { kind: 'visible' });
|
assert.deepEqual(mainWindow, { kind: 'visible' });
|
||||||
|
|
||||||
assert.deepEqual(runtime.createInvisibleWindow(), { kind: 'invisible' });
|
|
||||||
assert.deepEqual(invisibleWindow, { kind: 'invisible' });
|
|
||||||
|
|
||||||
assert.deepEqual(runtime.createSecondaryWindow(), { kind: 'secondary' });
|
|
||||||
assert.deepEqual(secondaryWindow, { kind: 'secondary' });
|
|
||||||
assert.deepEqual(runtime.createModalWindow(), { kind: 'modal' });
|
assert.deepEqual(runtime.createModalWindow(), { kind: 'modal' });
|
||||||
assert.deepEqual(modalWindow, { kind: 'modal' });
|
assert.deepEqual(modalWindow, { kind: 'modal' });
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
createCreateInvisibleWindowHandler,
|
|
||||||
createCreateMainWindowHandler,
|
createCreateMainWindowHandler,
|
||||||
createCreateModalWindowHandler,
|
createCreateModalWindowHandler,
|
||||||
createCreateOverlayWindowHandler,
|
createCreateOverlayWindowHandler,
|
||||||
createCreateSecondaryWindowHandler,
|
|
||||||
} from './overlay-window-factory';
|
} from './overlay-window-factory';
|
||||||
import {
|
import {
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler,
|
|
||||||
createBuildCreateMainWindowMainDepsHandler,
|
createBuildCreateMainWindowMainDepsHandler,
|
||||||
createBuildCreateModalWindowMainDepsHandler,
|
createBuildCreateModalWindowMainDepsHandler,
|
||||||
createBuildCreateOverlayWindowMainDepsHandler,
|
createBuildCreateOverlayWindowMainDepsHandler,
|
||||||
createBuildCreateSecondaryWindowMainDepsHandler,
|
|
||||||
} from './overlay-window-factory-main-deps';
|
} from './overlay-window-factory-main-deps';
|
||||||
|
|
||||||
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
||||||
@@ -20,8 +16,6 @@ type CreateOverlayWindowMainDeps<TWindow> = Parameters<
|
|||||||
export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
||||||
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
createOverlayWindowDeps: CreateOverlayWindowMainDeps<TWindow>;
|
||||||
setMainWindow: (window: TWindow | null) => void;
|
setMainWindow: (window: TWindow | null) => void;
|
||||||
setInvisibleWindow: (window: TWindow | null) => void;
|
|
||||||
setSecondaryWindow: (window: TWindow | null) => void;
|
|
||||||
setModalWindow: (window: TWindow | null) => void;
|
setModalWindow: (window: TWindow | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
const createOverlayWindow = createCreateOverlayWindowHandler<TWindow>(
|
||||||
@@ -33,18 +27,6 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
setMainWindow: (window) => deps.setMainWindow(window),
|
setMainWindow: (window) => deps.setMainWindow(window),
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
const createInvisibleWindow = createCreateInvisibleWindowHandler<TWindow>(
|
|
||||||
createBuildCreateInvisibleWindowMainDepsHandler<TWindow>({
|
|
||||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
|
||||||
setInvisibleWindow: (window) => deps.setInvisibleWindow(window),
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
const createSecondaryWindow = createCreateSecondaryWindowHandler<TWindow>(
|
|
||||||
createBuildCreateSecondaryWindowMainDepsHandler<TWindow>({
|
|
||||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
|
||||||
setSecondaryWindow: (window) => deps.setSecondaryWindow(window),
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
const createModalWindow = createCreateModalWindowHandler<TWindow>(
|
const createModalWindow = createCreateModalWindowHandler<TWindow>(
|
||||||
createBuildCreateModalWindowMainDepsHandler<TWindow>({
|
createBuildCreateModalWindowMainDepsHandler<TWindow>({
|
||||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||||
@@ -55,8 +37,6 @@ export function createOverlayWindowRuntimeHandlers<TWindow>(deps: {
|
|||||||
return {
|
return {
|
||||||
createOverlayWindow,
|
createOverlayWindow,
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
createInvisibleWindow,
|
|
||||||
createSecondaryWindow,
|
|
||||||
createModalWindow,
|
createModalWindow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,8 +156,6 @@ export interface AppState {
|
|||||||
currentSubText: string;
|
currentSubText: string;
|
||||||
currentSubAssText: string;
|
currentSubAssText: string;
|
||||||
currentSubtitleData: SubtitleData | null;
|
currentSubtitleData: SubtitleData | null;
|
||||||
hoveredSubtitleTokenIndex: number | null;
|
|
||||||
hoveredSubtitleRevision: number;
|
|
||||||
windowTracker: BaseWindowTracker | null;
|
windowTracker: BaseWindowTracker | null;
|
||||||
subtitlePosition: SubtitlePosition | null;
|
subtitlePosition: SubtitlePosition | null;
|
||||||
currentMediaPath: string | null;
|
currentMediaPath: string | null;
|
||||||
@@ -173,6 +171,9 @@ export interface AppState {
|
|||||||
secondarySubMode: SecondarySubMode;
|
secondarySubMode: SecondarySubMode;
|
||||||
lastSecondarySubToggleAtMs: number;
|
lastSecondarySubToggleAtMs: number;
|
||||||
previousSecondarySubVisibility: boolean | null;
|
previousSecondarySubVisibility: boolean | null;
|
||||||
|
overlaySavedMpvSubVisibility: boolean | null;
|
||||||
|
overlaySavedSecondaryMpvSubVisibility: boolean | null;
|
||||||
|
overlayMpvSubVisibilityRevision: number;
|
||||||
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
|
mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics;
|
||||||
shortcutsRegistered: boolean;
|
shortcutsRegistered: boolean;
|
||||||
overlayRuntimeInitialized: boolean;
|
overlayRuntimeInitialized: boolean;
|
||||||
@@ -230,8 +231,6 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
|||||||
currentSubText: '',
|
currentSubText: '',
|
||||||
currentSubAssText: '',
|
currentSubAssText: '',
|
||||||
currentSubtitleData: null,
|
currentSubtitleData: null,
|
||||||
hoveredSubtitleTokenIndex: null,
|
|
||||||
hoveredSubtitleRevision: 0,
|
|
||||||
windowTracker: null,
|
windowTracker: null,
|
||||||
subtitlePosition: null,
|
subtitlePosition: null,
|
||||||
currentMediaPath: null,
|
currentMediaPath: null,
|
||||||
@@ -247,6 +246,9 @@ export function createAppState(values: AppStateInitialValues): AppState {
|
|||||||
secondarySubMode: 'hover',
|
secondarySubMode: 'hover',
|
||||||
lastSecondarySubToggleAtMs: 0,
|
lastSecondarySubToggleAtMs: 0,
|
||||||
previousSecondarySubVisibility: null,
|
previousSecondarySubVisibility: null,
|
||||||
|
overlaySavedMpvSubVisibility: null,
|
||||||
|
overlaySavedSecondaryMpvSubVisibility: null,
|
||||||
|
overlayMpvSubVisibilityRevision: 0,
|
||||||
mpvSubtitleRenderMetrics: {
|
mpvSubtitleRenderMetrics: {
|
||||||
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
...DEFAULT_MPV_SUBTITLE_RENDER_METRICS,
|
||||||
},
|
},
|
||||||
|
|||||||
133
src/preload.ts
133
src/preload.ts
@@ -45,7 +45,6 @@ import type {
|
|||||||
RuntimeOptionId,
|
RuntimeOptionId,
|
||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
RuntimeOptionValue,
|
RuntimeOptionValue,
|
||||||
MpvSubtitleRenderMetrics,
|
|
||||||
OverlayContentMeasurement,
|
OverlayContentMeasurement,
|
||||||
ShortcutsConfig,
|
ShortcutsConfig,
|
||||||
ConfigHotReloadPayload,
|
ConfigHotReloadPayload,
|
||||||
@@ -55,12 +54,80 @@ import { IPC_CHANNELS } from './shared/ipc/contracts';
|
|||||||
const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer='));
|
const overlayLayerArg = process.argv.find((arg) => arg.startsWith('--overlay-layer='));
|
||||||
const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
const overlayLayerFromArg = overlayLayerArg?.slice('--overlay-layer='.length);
|
||||||
const overlayLayer =
|
const overlayLayer =
|
||||||
overlayLayerFromArg === 'visible' ||
|
overlayLayerFromArg === 'visible' || overlayLayerFromArg === 'modal' ? overlayLayerFromArg : null;
|
||||||
overlayLayerFromArg === 'invisible' ||
|
|
||||||
overlayLayerFromArg === 'secondary' ||
|
type EmptyListener = () => void;
|
||||||
overlayLayerFromArg === 'modal'
|
type PayloadedListener<T> = (payload: T) => void;
|
||||||
? overlayLayerFromArg
|
|
||||||
: null;
|
function createQueuedIpcListener(
|
||||||
|
channel: string,
|
||||||
|
): (listener: EmptyListener) => void {
|
||||||
|
let count = 0;
|
||||||
|
const listeners: EmptyListener[] = [];
|
||||||
|
|
||||||
|
const dispatch = (): void => {
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
count += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.on(channel, () => {
|
||||||
|
dispatch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (listener: EmptyListener): void => {
|
||||||
|
listeners.push(listener);
|
||||||
|
while (count > 0) {
|
||||||
|
count -= 1;
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQueuedIpcListenerWithPayload<T>(
|
||||||
|
channel: string,
|
||||||
|
normalize: (payload: unknown) => T,
|
||||||
|
): (listener: PayloadedListener<T>) => void {
|
||||||
|
const pending: T[] = [];
|
||||||
|
const listeners: PayloadedListener<T>[] = [];
|
||||||
|
|
||||||
|
const dispatch = (payload: T): void => {
|
||||||
|
if (listeners.length === 0) {
|
||||||
|
pending.push(payload);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ipcRenderer.on(channel, (_event: IpcRendererEvent, payloadArg: unknown) => {
|
||||||
|
dispatch(normalize(payloadArg));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (listener: PayloadedListener<T>): void => {
|
||||||
|
listeners.push(listener);
|
||||||
|
while (pending.length > 0) {
|
||||||
|
const payload = pending.shift();
|
||||||
|
listener(payload as T);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpenRuntimeOptionsEvent = createQueuedIpcListener(IPC_CHANNELS.event.runtimeOptionsOpen);
|
||||||
|
const onOpenJimakuEvent = createQueuedIpcListener(IPC_CHANNELS.event.jimakuOpen);
|
||||||
|
const onSubsyncManualOpenEvent = createQueuedIpcListenerWithPayload<SubsyncManualPayload>(
|
||||||
|
IPC_CHANNELS.event.subsyncOpenManual,
|
||||||
|
(payload) => payload as SubsyncManualPayload,
|
||||||
|
);
|
||||||
|
const onKikuFieldGroupingRequestEvent = createQueuedIpcListenerWithPayload<KikuFieldGroupingRequestData>(
|
||||||
|
IPC_CHANNELS.event.kikuFieldGroupingRequest,
|
||||||
|
(payload) => payload as KikuFieldGroupingRequestData,
|
||||||
|
);
|
||||||
|
|
||||||
const electronAPI: ElectronAPI = {
|
const electronAPI: ElectronAPI = {
|
||||||
getOverlayLayer: () => overlayLayer,
|
getOverlayLayer: () => overlayLayer,
|
||||||
@@ -94,16 +161,6 @@ const electronAPI: ElectronAPI = {
|
|||||||
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleRaw),
|
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleRaw),
|
||||||
getCurrentSubtitleAss: (): Promise<string> =>
|
getCurrentSubtitleAss: (): Promise<string> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleAss),
|
ipcRenderer.invoke(IPC_CHANNELS.request.getCurrentSubtitleAss),
|
||||||
getMpvSubtitleRenderMetrics: () =>
|
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.getMpvSubtitleRenderMetrics),
|
|
||||||
onMpvSubtitleRenderMetrics: (callback: (metrics: MpvSubtitleRenderMetrics) => void) => {
|
|
||||||
ipcRenderer.on(
|
|
||||||
IPC_CHANNELS.event.mpvSubtitleRenderMetricsSet,
|
|
||||||
(_event: IpcRendererEvent, metrics: MpvSubtitleRenderMetrics) => {
|
|
||||||
callback(metrics);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSubtitleAss: (callback: (assText: string) => void) => {
|
onSubtitleAss: (callback: (assText: string) => void) => {
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
IPC_CHANNELS.event.subtitleAssSet,
|
IPC_CHANNELS.event.subtitleAssSet,
|
||||||
@@ -112,14 +169,6 @@ const electronAPI: ElectronAPI = {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onOverlayDebugVisualization: (callback: (enabled: boolean) => void) => {
|
|
||||||
ipcRenderer.on(
|
|
||||||
IPC_CHANNELS.event.overlayDebugVisualizationSet,
|
|
||||||
(_event: IpcRendererEvent, enabled: boolean) => {
|
|
||||||
callback(enabled);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => {
|
||||||
ipcRenderer.send(IPC_CHANNELS.command.setIgnoreMouseEvents, ignore, options);
|
ipcRenderer.send(IPC_CHANNELS.command.setIgnoreMouseEvents, ignore, options);
|
||||||
@@ -201,23 +250,11 @@ const electronAPI: ElectronAPI = {
|
|||||||
focusMainWindow: () => ipcRenderer.invoke(IPC_CHANNELS.request.focusMainWindow) as Promise<void>,
|
focusMainWindow: () => ipcRenderer.invoke(IPC_CHANNELS.request.focusMainWindow) as Promise<void>,
|
||||||
getSubtitleStyle: (): Promise<SubtitleStyleConfig | null> =>
|
getSubtitleStyle: (): Promise<SubtitleStyleConfig | null> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitleStyle),
|
ipcRenderer.invoke(IPC_CHANNELS.request.getSubtitleStyle),
|
||||||
onSubsyncManualOpen: (callback: (payload: SubsyncManualPayload) => void) => {
|
onSubsyncManualOpen: onSubsyncManualOpenEvent,
|
||||||
ipcRenderer.on(
|
|
||||||
IPC_CHANNELS.event.subsyncOpenManual,
|
|
||||||
(_event: IpcRendererEvent, payload: SubsyncManualPayload) => {
|
|
||||||
callback(payload);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
runSubsyncManual: (request: SubsyncManualRunRequest): Promise<SubsyncResult> =>
|
runSubsyncManual: (request: SubsyncManualRunRequest): Promise<SubsyncResult> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.runSubsyncManual, request),
|
ipcRenderer.invoke(IPC_CHANNELS.request.runSubsyncManual, request),
|
||||||
|
|
||||||
onKikuFieldGroupingRequest: (callback: (data: KikuFieldGroupingRequestData) => void) => {
|
onKikuFieldGroupingRequest: onKikuFieldGroupingRequestEvent,
|
||||||
ipcRenderer.on(
|
|
||||||
IPC_CHANNELS.event.kikuFieldGroupingRequest,
|
|
||||||
(_event: IpcRendererEvent, data: KikuFieldGroupingRequestData) => callback(data),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
kikuBuildMergePreview: (request: KikuMergePreviewRequest): Promise<KikuMergePreviewResponse> =>
|
kikuBuildMergePreview: (request: KikuMergePreviewRequest): Promise<KikuMergePreviewResponse> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.kikuBuildMergePreview, request),
|
ipcRenderer.invoke(IPC_CHANNELS.request.kikuBuildMergePreview, request),
|
||||||
|
|
||||||
@@ -242,27 +279,19 @@ const electronAPI: ElectronAPI = {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onOpenRuntimeOptions: (callback: () => void) => {
|
onOpenRuntimeOptions: onOpenRuntimeOptionsEvent,
|
||||||
ipcRenderer.on(IPC_CHANNELS.event.runtimeOptionsOpen, () => {
|
onOpenJimaku: onOpenJimakuEvent,
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onOpenJimaku: (callback: () => void) => {
|
|
||||||
ipcRenderer.on(IPC_CHANNELS.event.jimakuOpen, () => {
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
appendClipboardVideoToQueue: (): Promise<ClipboardAppendResult> =>
|
||||||
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
ipcRenderer.invoke(IPC_CHANNELS.request.appendClipboardVideoToQueue),
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
||||||
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal);
|
ipcRenderer.send(IPC_CHANNELS.command.overlayModalClosed, modal);
|
||||||
},
|
},
|
||||||
|
notifyOverlayModalOpened: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => {
|
||||||
|
ipcRenderer.send(IPC_CHANNELS.command.overlayModalOpened, modal);
|
||||||
|
},
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => {
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => {
|
||||||
ipcRenderer.send(IPC_CHANNELS.command.reportOverlayContentBounds, measurement);
|
ipcRenderer.send(IPC_CHANNELS.command.reportOverlayContentBounds, measurement);
|
||||||
},
|
},
|
||||||
reportHoveredSubtitleToken: (tokenIndex: number | null) => {
|
|
||||||
ipcRenderer.send('subtitle-token-hover:set', tokenIndex);
|
|
||||||
},
|
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => {
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => {
|
||||||
ipcRenderer.on(
|
ipcRenderer.on(
|
||||||
IPC_CHANNELS.event.configHotReload,
|
IPC_CHANNELS.event.configHotReload,
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
import { createRendererRecoveryController } from './error-recovery.js';
|
import { createRendererRecoveryController } from './error-recovery.js';
|
||||||
|
import {
|
||||||
|
YOMITAN_POPUP_IFRAME_SELECTOR,
|
||||||
|
hasYomitanPopupIframe,
|
||||||
|
isYomitanPopupIframe,
|
||||||
|
} from './yomitan-popup.js';
|
||||||
import { resolvePlatformInfo } from './utils/platform.js';
|
import { resolvePlatformInfo } from './utils/platform.js';
|
||||||
|
|
||||||
test('handleError logs context and recovers overlay state', () => {
|
test('handleError logs context and recovers overlay state', () => {
|
||||||
@@ -26,7 +31,6 @@ test('handleError logs context and recovers overlay state', () => {
|
|||||||
secondarySubtitlePreview: 'secondary',
|
secondarySubtitlePreview: 'secondary',
|
||||||
isOverlayInteractive: true,
|
isOverlayInteractive: true,
|
||||||
isOverSubtitle: true,
|
isOverSubtitle: true,
|
||||||
invisiblePositionEditMode: false,
|
|
||||||
overlayLayer: 'visible',
|
overlayLayer: 'visible',
|
||||||
}),
|
}),
|
||||||
logError: (payload) => {
|
logError: (payload) => {
|
||||||
@@ -72,8 +76,7 @@ test('handleError normalizes non-Error values', () => {
|
|||||||
secondarySubtitlePreview: '',
|
secondarySubtitlePreview: '',
|
||||||
isOverlayInteractive: false,
|
isOverlayInteractive: false,
|
||||||
isOverSubtitle: false,
|
isOverSubtitle: false,
|
||||||
invisiblePositionEditMode: false,
|
overlayLayer: 'visible',
|
||||||
overlayLayer: 'invisible',
|
|
||||||
}),
|
}),
|
||||||
logError: (payload) => {
|
logError: (payload) => {
|
||||||
payloads.push(payload);
|
payloads.push(payload);
|
||||||
@@ -107,7 +110,6 @@ test('nested recovery errors are ignored while current recovery is active', () =
|
|||||||
secondarySubtitlePreview: '',
|
secondarySubtitlePreview: '',
|
||||||
isOverlayInteractive: true,
|
isOverlayInteractive: true,
|
||||||
isOverSubtitle: false,
|
isOverSubtitle: false,
|
||||||
invisiblePositionEditMode: true,
|
|
||||||
overlayLayer: 'visible',
|
overlayLayer: 'visible',
|
||||||
}),
|
}),
|
||||||
logError: (payload) => {
|
logError: (payload) => {
|
||||||
@@ -130,7 +132,7 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
value: {
|
value: {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
getOverlayLayer: () => 'invisible',
|
getOverlayLayer: () => 'modal',
|
||||||
},
|
},
|
||||||
location: { search: '?layer=visible' },
|
location: { search: '?layer=visible' },
|
||||||
},
|
},
|
||||||
@@ -146,7 +148,6 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
|||||||
try {
|
try {
|
||||||
const info = resolvePlatformInfo();
|
const info = resolvePlatformInfo();
|
||||||
assert.equal(info.overlayLayer, 'visible');
|
assert.equal(info.overlayLayer, 'visible');
|
||||||
assert.equal(info.isInvisibleLayer, false);
|
|
||||||
} finally {
|
} finally {
|
||||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
Object.defineProperty(globalThis, 'navigator', {
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
@@ -156,7 +157,7 @@ test('resolvePlatformInfo prefers query layer over preload layer', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolvePlatformInfo supports secondary layer and disables mouse-ignore toggles', () => {
|
test('resolvePlatformInfo ignores legacy secondary layer and falls back to visible', () => {
|
||||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
const previousNavigator = (globalThis as { navigator?: unknown }).navigator;
|
const previousNavigator = (globalThis as { navigator?: unknown }).navigator;
|
||||||
|
|
||||||
@@ -179,9 +180,8 @@ test('resolvePlatformInfo supports secondary layer and disables mouse-ignore tog
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const info = resolvePlatformInfo();
|
const info = resolvePlatformInfo();
|
||||||
assert.equal(info.overlayLayer, 'secondary');
|
assert.equal(info.overlayLayer, 'visible');
|
||||||
assert.equal(info.isSecondaryLayer, true);
|
assert.equal(info.shouldToggleMouseIgnore, true);
|
||||||
assert.equal(info.shouldToggleMouseIgnore, false);
|
|
||||||
} finally {
|
} finally {
|
||||||
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
|
||||||
Object.defineProperty(globalThis, 'navigator', {
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
@@ -225,3 +225,59 @@ test('resolvePlatformInfo supports modal layer and disables mouse-ignore toggles
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('isYomitanPopupIframe matches modern popup class and legacy id prefix', () => {
|
||||||
|
const createElement = (options: {
|
||||||
|
tagName: string;
|
||||||
|
id?: string;
|
||||||
|
classNames?: string[];
|
||||||
|
}): Element =>
|
||||||
|
({
|
||||||
|
tagName: options.tagName,
|
||||||
|
id: options.id ?? '',
|
||||||
|
classList: {
|
||||||
|
contains: (className: string) => (options.classNames ?? []).includes(className),
|
||||||
|
},
|
||||||
|
}) as unknown as Element;
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
isYomitanPopupIframe(
|
||||||
|
createElement({
|
||||||
|
tagName: 'IFRAME',
|
||||||
|
classNames: ['yomitan-popup'],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isYomitanPopupIframe(
|
||||||
|
createElement({
|
||||||
|
tagName: 'IFRAME',
|
||||||
|
id: 'yomitan-popup-123',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isYomitanPopupIframe(
|
||||||
|
createElement({
|
||||||
|
tagName: 'IFRAME',
|
||||||
|
id: 'something-else',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasYomitanPopupIframe queries for modern + legacy selector', () => {
|
||||||
|
let selector = '';
|
||||||
|
const root = {
|
||||||
|
querySelector: (value: string) => {
|
||||||
|
selector = value;
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
} as unknown as ParentNode;
|
||||||
|
|
||||||
|
assert.equal(hasYomitanPopupIframe(root), true);
|
||||||
|
assert.equal(selector, YOMITAN_POPUP_IFRAME_SELECTOR);
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export type RendererRecoverySnapshot = {
|
|||||||
secondarySubtitlePreview: string;
|
secondarySubtitlePreview: string;
|
||||||
isOverlayInteractive: boolean;
|
isOverlayInteractive: boolean;
|
||||||
isOverSubtitle: boolean;
|
isOverSubtitle: boolean;
|
||||||
invisiblePositionEditMode: boolean;
|
overlayLayer: 'visible' | 'modal';
|
||||||
overlayLayer: 'visible' | 'invisible' | 'secondary' | 'modal';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type NormalizedRendererError = {
|
type NormalizedRendererError = {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
KikuDuplicateCardInfo,
|
KikuDuplicateCardInfo,
|
||||||
MpvSubtitleRenderMetrics,
|
|
||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
SecondarySubMode,
|
SecondarySubMode,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
@@ -84,10 +83,7 @@ function syncSettingsModalSubtitleSuppression(): void {
|
|||||||
|
|
||||||
const subtitleRenderer = createSubtitleRenderer(ctx);
|
const subtitleRenderer = createSubtitleRenderer(ctx);
|
||||||
const measurementReporter = createOverlayContentMeasurementReporter(ctx);
|
const measurementReporter = createOverlayContentMeasurementReporter(ctx);
|
||||||
const positioning = createPositioningController(ctx, {
|
const positioning = createPositioningController(ctx);
|
||||||
modalStateReader: { isAnySettingsModalOpen },
|
|
||||||
applySubtitleFontSize: subtitleRenderer.applySubtitleFontSize,
|
|
||||||
});
|
|
||||||
const runtimeOptionsModal = createRuntimeOptionsModal(ctx, {
|
const runtimeOptionsModal = createRuntimeOptionsModal(ctx, {
|
||||||
modalStateReader: { isAnyModalOpen },
|
modalStateReader: { isAnyModalOpen },
|
||||||
syncSettingsModalSubtitleSuppression,
|
syncSettingsModalSubtitleSuppression,
|
||||||
@@ -115,25 +111,15 @@ const keyboardHandlers = createKeyboardHandlers(ctx, {
|
|||||||
handleJimakuKeydown: jimakuModal.handleJimakuKeydown,
|
handleJimakuKeydown: jimakuModal.handleJimakuKeydown,
|
||||||
handleSessionHelpKeydown: sessionHelpModal.handleSessionHelpKeydown,
|
handleSessionHelpKeydown: sessionHelpModal.handleSessionHelpKeydown,
|
||||||
openSessionHelpModal: sessionHelpModal.openSessionHelpModal,
|
openSessionHelpModal: sessionHelpModal.openSessionHelpModal,
|
||||||
saveInvisiblePositionEdit: positioning.saveInvisiblePositionEdit,
|
|
||||||
cancelInvisiblePositionEdit: positioning.cancelInvisiblePositionEdit,
|
|
||||||
setInvisiblePositionEditMode: positioning.setInvisiblePositionEditMode,
|
|
||||||
applyInvisibleSubtitleOffsetPosition: positioning.applyInvisibleSubtitleOffsetPosition,
|
|
||||||
updateInvisiblePositionEditHud: positioning.updateInvisiblePositionEditHud,
|
|
||||||
appendClipboardVideoToQueue: () => {
|
appendClipboardVideoToQueue: () => {
|
||||||
void window.electronAPI.appendClipboardVideoToQueue();
|
void window.electronAPI.appendClipboardVideoToQueue();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const mouseHandlers = createMouseHandlers(ctx, {
|
const mouseHandlers = createMouseHandlers(ctx, {
|
||||||
modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
|
modalStateReader: { isAnySettingsModalOpen, isAnyModalOpen },
|
||||||
applyInvisibleSubtitleLayoutFromMpvMetrics:
|
|
||||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics,
|
|
||||||
applyYPercent: positioning.applyYPercent,
|
applyYPercent: positioning.applyYPercent,
|
||||||
getCurrentYPercent: positioning.getCurrentYPercent,
|
getCurrentYPercent: positioning.getCurrentYPercent,
|
||||||
persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
|
persistSubtitlePositionPatch: positioning.persistSubtitlePositionPatch,
|
||||||
reportHoveredTokenIndex: (tokenIndex: number | null) => {
|
|
||||||
window.electronAPI.reportHoveredSubtitleToken(tokenIndex);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastSubtitlePreview = '';
|
let lastSubtitlePreview = '';
|
||||||
@@ -179,9 +165,6 @@ function dismissActiveUiAfterError(): void {
|
|||||||
|
|
||||||
function restoreOverlayInteractionAfterError(): void {
|
function restoreOverlayInteractionAfterError(): void {
|
||||||
ctx.state.isOverSubtitle = false;
|
ctx.state.isOverSubtitle = false;
|
||||||
if (ctx.state.invisiblePositionEditMode) {
|
|
||||||
positioning.setInvisiblePositionEditMode(false);
|
|
||||||
}
|
|
||||||
ctx.dom.overlay.classList.remove('interactive');
|
ctx.dom.overlay.classList.remove('interactive');
|
||||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||||
@@ -212,7 +195,6 @@ const recovery = createRendererRecoveryController({
|
|||||||
secondarySubtitlePreview: lastSecondarySubtitlePreview,
|
secondarySubtitlePreview: lastSecondarySubtitlePreview,
|
||||||
isOverlayInteractive: ctx.dom.overlay.classList.contains('interactive'),
|
isOverlayInteractive: ctx.dom.overlay.classList.contains('interactive'),
|
||||||
isOverSubtitle: ctx.state.isOverSubtitle,
|
isOverSubtitle: ctx.state.isOverSubtitle,
|
||||||
invisiblePositionEditMode: ctx.state.invisiblePositionEditMode,
|
|
||||||
overlayLayer: ctx.platform.overlayLayer,
|
overlayLayer: ctx.platform.overlayLayer,
|
||||||
}),
|
}),
|
||||||
logError: (payload) => {
|
logError: (payload) => {
|
||||||
@@ -222,6 +204,41 @@ const recovery = createRendererRecoveryController({
|
|||||||
|
|
||||||
registerRendererGlobalErrorHandlers(window, recovery);
|
registerRendererGlobalErrorHandlers(window, recovery);
|
||||||
|
|
||||||
|
function registerModalOpenHandlers(): void {
|
||||||
|
window.electronAPI.onOpenRuntimeOptions(() => {
|
||||||
|
runGuardedAsync('runtime-options:open', async () => {
|
||||||
|
try {
|
||||||
|
await runtimeOptionsModal.openRuntimeOptionsModal();
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('runtime-options');
|
||||||
|
} catch {
|
||||||
|
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
||||||
|
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
||||||
|
syncSettingsModalSubtitleSuppression();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.electronAPI.onOpenJimaku(() => {
|
||||||
|
runGuarded('jimaku:open', () => {
|
||||||
|
jimakuModal.openJimakuModal();
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('jimaku');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
|
||||||
|
runGuarded('subsync:manual-open', () => {
|
||||||
|
subsyncModal.openSubsyncModal(payload);
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('subsync');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
window.electronAPI.onKikuFieldGroupingRequest(
|
||||||
|
(data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
|
||||||
|
runGuarded('kiku:field-grouping-open', () => {
|
||||||
|
kikuModal.openKikuFieldGroupingModal(data);
|
||||||
|
window.electronAPI.notifyOverlayModalOpened('kiku');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function runGuarded(action: string, fn: () => void): void {
|
function runGuarded(action: string, fn: () => void): void {
|
||||||
try {
|
try {
|
||||||
fn();
|
fn();
|
||||||
@@ -238,6 +255,8 @@ function runGuardedAsync(action: string, fn: () => Promise<void> | void): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerModalOpenHandlers();
|
||||||
|
|
||||||
async function init(): Promise<void> {
|
async function init(): Promise<void> {
|
||||||
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
document.body.classList.add(`layer-${ctx.platform.overlayLayer}`);
|
||||||
if (ctx.platform.isMacOSPlatform) {
|
if (ctx.platform.isMacOSPlatform) {
|
||||||
@@ -252,41 +271,17 @@ async function init(): Promise<void> {
|
|||||||
lastSubtitlePreview = truncateForErrorLog(data.text);
|
lastSubtitlePreview = truncateForErrorLog(data.text);
|
||||||
}
|
}
|
||||||
subtitleRenderer.renderSubtitle(data);
|
subtitleRenderer.renderSubtitle(data);
|
||||||
if (ctx.platform.isInvisibleLayer && ctx.state.mpvSubtitleRenderMetrics) {
|
|
||||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
|
||||||
ctx.state.mpvSubtitleRenderMetrics,
|
|
||||||
'subtitle-change',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
|
window.electronAPI.onSubtitlePosition((position: SubtitlePosition | null) => {
|
||||||
runGuarded('subtitle-position:update', () => {
|
runGuarded('subtitle-position:update', () => {
|
||||||
if (ctx.platform.isInvisibleLayer) {
|
positioning.applyStoredSubtitlePosition(position, 'media-change');
|
||||||
positioning.applyInvisibleStoredSubtitlePosition(position, 'media-change');
|
|
||||||
} else {
|
|
||||||
positioning.applyStoredSubtitlePosition(position, 'media-change');
|
|
||||||
}
|
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.platform.isInvisibleLayer) {
|
|
||||||
window.electronAPI.onMpvSubtitleRenderMetrics((metrics: MpvSubtitleRenderMetrics) => {
|
|
||||||
runGuarded('mpv-metrics:update', () => {
|
|
||||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(metrics, 'event');
|
|
||||||
measurementReporter.schedule();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.electronAPI.onOverlayDebugVisualization((enabled: boolean) => {
|
|
||||||
runGuarded('overlay-debug-visualization:update', () => {
|
|
||||||
document.body.classList.toggle('debug-invisible-visualization', enabled);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
|
const initialSubtitle = await window.electronAPI.getCurrentSubtitleRaw();
|
||||||
lastSubtitlePreview = truncateForErrorLog(initialSubtitle);
|
lastSubtitlePreview = truncateForErrorLog(initialSubtitle);
|
||||||
subtitleRenderer.renderSubtitle(initialSubtitle);
|
subtitleRenderer.renderSubtitle(initialSubtitle);
|
||||||
@@ -310,17 +305,11 @@ async function init(): Promise<void> {
|
|||||||
subtitleRenderer.renderSecondarySub(await window.electronAPI.getCurrentSecondarySub());
|
subtitleRenderer.renderSecondarySub(await window.electronAPI.getCurrentSecondarySub());
|
||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
|
|
||||||
const hoverTarget = ctx.platform.isInvisibleLayer
|
ctx.dom.subtitleContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||||
? ctx.dom.subtitleRoot
|
ctx.dom.subtitleContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||||
: ctx.dom.subtitleContainer;
|
|
||||||
hoverTarget.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
|
||||||
hoverTarget.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
|
||||||
ctx.dom.secondarySubContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
ctx.dom.secondarySubContainer.addEventListener('mouseenter', mouseHandlers.handleMouseEnter);
|
||||||
ctx.dom.secondarySubContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
ctx.dom.secondarySubContainer.addEventListener('mouseleave', mouseHandlers.handleMouseLeave);
|
||||||
|
|
||||||
mouseHandlers.setupInvisibleHoverSelection();
|
|
||||||
mouseHandlers.setupInvisibleTokenHoverReporter();
|
|
||||||
positioning.setupInvisiblePositionEditHud();
|
|
||||||
mouseHandlers.setupResizeHandler();
|
mouseHandlers.setupResizeHandler();
|
||||||
mouseHandlers.setupSelectionObserver();
|
mouseHandlers.setupSelectionObserver();
|
||||||
mouseHandlers.setupYomitanObserver();
|
mouseHandlers.setupYomitanObserver();
|
||||||
@@ -348,59 +337,14 @@ async function init(): Promise<void> {
|
|||||||
measurementReporter.schedule();
|
measurementReporter.schedule();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
window.electronAPI.onOpenRuntimeOptions(() => {
|
mouseHandlers.setupDragging();
|
||||||
runGuardedAsync('runtime-options:open', async () => {
|
|
||||||
try {
|
|
||||||
await runtimeOptionsModal.openRuntimeOptionsModal();
|
|
||||||
} catch {
|
|
||||||
runtimeOptionsModal.setRuntimeOptionsStatus('Failed to load runtime options', true);
|
|
||||||
window.electronAPI.notifyOverlayModalClosed('runtime-options');
|
|
||||||
syncSettingsModalSubtitleSuppression();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.electronAPI.onOpenJimaku(() => {
|
|
||||||
runGuarded('jimaku:open', () => {
|
|
||||||
jimakuModal.openJimakuModal();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.electronAPI.onSubsyncManualOpen((payload: SubsyncManualPayload) => {
|
|
||||||
runGuarded('subsync:manual-open', () => {
|
|
||||||
subsyncModal.openSubsyncModal(payload);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
window.electronAPI.onKikuFieldGroupingRequest(
|
|
||||||
(data: { original: KikuDuplicateCardInfo; duplicate: KikuDuplicateCardInfo }) => {
|
|
||||||
runGuarded('kiku:field-grouping-open', () => {
|
|
||||||
kikuModal.openKikuFieldGroupingModal(data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ctx.platform.isInvisibleLayer) {
|
|
||||||
mouseHandlers.setupDragging();
|
|
||||||
}
|
|
||||||
|
|
||||||
await keyboardHandlers.setupMpvInputForwarding();
|
await keyboardHandlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle());
|
subtitleRenderer.applySubtitleStyle(await window.electronAPI.getSubtitleStyle());
|
||||||
|
|
||||||
if (ctx.platform.isInvisibleLayer) {
|
positioning.applyStoredSubtitlePosition(await window.electronAPI.getSubtitlePosition(), 'startup');
|
||||||
positioning.applyInvisibleStoredSubtitlePosition(
|
measurementReporter.schedule();
|
||||||
await window.electronAPI.getSubtitlePosition(),
|
|
||||||
'startup',
|
|
||||||
);
|
|
||||||
positioning.applyInvisibleSubtitleLayoutFromMpvMetrics(
|
|
||||||
await window.electronAPI.getMpvSubtitleRenderMetrics(),
|
|
||||||
'startup',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
positioning.applyStoredSubtitlePosition(
|
|
||||||
await window.electronAPI.getSubtitlePosition(),
|
|
||||||
'startup',
|
|
||||||
);
|
|
||||||
measurementReporter.schedule();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.platform.shouldToggleMouseIgnore) {
|
if (ctx.platform.shouldToggleMouseIgnore) {
|
||||||
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
window.electronAPI.setIgnoreMouseEvents(true, { forward: true });
|
||||||
@@ -421,7 +365,7 @@ function setupDragDropToMpvQueue(): void {
|
|||||||
|
|
||||||
const clearDropInteractive = (): void => {
|
const clearDropInteractive = (): void => {
|
||||||
dragDepth = 0;
|
dragDepth = 0;
|
||||||
if (isAnyModalOpen() || ctx.state.isOverSubtitle || ctx.state.invisiblePositionEditMode) {
|
if (isAnyModalOpen() || ctx.state.isOverSubtitle) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.dom.overlay.classList.remove('interactive');
|
ctx.dom.overlay.classList.remove('interactive');
|
||||||
|
|||||||
@@ -280,6 +280,8 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 35px;
|
font-size: 35px;
|
||||||
line-height: var(--visible-sub-line-height, 1.32);
|
line-height: var(--visible-sub-line-height, 1.32);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: keep-all;
|
||||||
color: #cad3f5;
|
color: #cad3f5;
|
||||||
--subtitle-known-word-color: #a6da95;
|
--subtitle-known-word-color: #a6da95;
|
||||||
--subtitle-n-plus-one-color: #c6a0f6;
|
--subtitle-n-plus-one-color: #c6a0f6;
|
||||||
@@ -288,6 +290,8 @@ body {
|
|||||||
--subtitle-jlpt-n3-color: #f9e2af;
|
--subtitle-jlpt-n3-color: #f9e2af;
|
||||||
--subtitle-jlpt-n4-color: #a6e3a1;
|
--subtitle-jlpt-n4-color: #a6e3a1;
|
||||||
--subtitle-jlpt-n5-color: #8aadf4;
|
--subtitle-jlpt-n5-color: #8aadf4;
|
||||||
|
--subtitle-hover-token-color: #f4dbd6;
|
||||||
|
--subtitle-hover-token-background-color: rgba(54, 58, 79, 0.84);
|
||||||
--subtitle-frequency-single-color: #f5a97f;
|
--subtitle-frequency-single-color: #f5a97f;
|
||||||
--subtitle-frequency-band-1-color: #ed8796;
|
--subtitle-frequency-band-1-color: #ed8796;
|
||||||
--subtitle-frequency-band-2-color: #f5a97f;
|
--subtitle-frequency-band-2-color: #f5a97f;
|
||||||
@@ -300,6 +304,7 @@ body {
|
|||||||
/* Enable text selection for Yomitan */
|
/* Enable text selection for Yomitan */
|
||||||
user-select: text;
|
user-select: text;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
-webkit-text-fill-color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot:empty {
|
#subtitleRoot:empty {
|
||||||
@@ -318,16 +323,21 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
#subtitleRoot .c {
|
#subtitleRoot .c {
|
||||||
display: inline;
|
display: inline;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
color: inherit;
|
||||||
|
-webkit-text-fill-color: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .c:hover {
|
#subtitleRoot .c:hover {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||||
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .word {
|
#subtitleRoot .word {
|
||||||
display: inline;
|
display: inline;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
-webkit-text-fill-color: currentColor !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .word.word-known {
|
#subtitleRoot .word.word-known {
|
||||||
@@ -418,9 +428,103 @@ body.settings-modal-open #subtitleContainer {
|
|||||||
color: var(--subtitle-frequency-band-5-color, #8aadf4);
|
color: var(--subtitle-frequency-band-5-color, #8aadf4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot .word:hover {
|
#subtitleRoot .word:not(.word-known):not(.word-n-plus-one):not(.word-frequency-single):not(
|
||||||
background: rgba(255, 255, 255, 0.2);
|
.word-frequency-band-1
|
||||||
|
):not(.word-frequency-band-2):not(.word-frequency-band-3):not(.word-frequency-band-4):not(
|
||||||
|
.word-frequency-band-5
|
||||||
|
):hover {
|
||||||
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-known:hover,
|
||||||
|
#subtitleRoot .word.word-n-plus-one:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-single:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-1:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-2:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-3:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-4:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-5:hover {
|
||||||
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-known .c:hover,
|
||||||
|
#subtitleRoot .word.word-n-plus-one .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-single .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-1 .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-2 .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-3 .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-4 .c:hover,
|
||||||
|
#subtitleRoot .word.word-frequency-band-5 .c:hover {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit !important;
|
||||||
|
-webkit-text-fill-color: currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot::selection,
|
||||||
|
#subtitleRoot .word::selection,
|
||||||
|
#subtitleRoot .c::selection {
|
||||||
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84));
|
||||||
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot *::selection {
|
||||||
|
background: var(--subtitle-hover-token-background-color, rgba(54, 58, 79, 0.84)) !important;
|
||||||
|
color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-hover-token-color, #f4dbd6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-known::selection,
|
||||||
|
#subtitleRoot .word.word-known .c::selection {
|
||||||
|
color: var(--subtitle-known-word-color, #a6da95) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-known-word-color, #a6da95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-n-plus-one::selection,
|
||||||
|
#subtitleRoot .word.word-n-plus-one .c::selection {
|
||||||
|
color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-n-plus-one-color, #c6a0f6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-single::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-single .c::selection {
|
||||||
|
color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-single-color, #f5a97f) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-band-1::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-band-1 .c::selection {
|
||||||
|
color: var(--subtitle-frequency-band-1-color, #ed8796) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-band-1-color, #ed8796) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-band-2::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-band-2 .c::selection {
|
||||||
|
color: var(--subtitle-frequency-band-2-color, #f5a97f) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-band-2-color, #f5a97f) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-band-3::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-band-3 .c::selection {
|
||||||
|
color: var(--subtitle-frequency-band-3-color, #f9e2af) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-band-3-color, #f9e2af) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-band-4::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-band-4 .c::selection {
|
||||||
|
color: var(--subtitle-frequency-band-4-color, #a6e3a1) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-band-4-color, #a6e3a1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleRoot .word.word-frequency-band-5::selection,
|
||||||
|
#subtitleRoot .word.word-frequency-band-5 .c::selection {
|
||||||
|
color: var(--subtitle-frequency-band-5-color, #8aadf4) !important;
|
||||||
|
-webkit-text-fill-color: var(--subtitle-frequency-band-5-color, #8aadf4) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#subtitleRoot br {
|
#subtitleRoot br {
|
||||||
@@ -439,93 +543,6 @@ body.platform-macos.layer-visible #subtitleRoot {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layer-invisible #subtitleContainer {
|
|
||||||
background: transparent !important;
|
|
||||||
border: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible #subtitleRoot,
|
|
||||||
body.layer-invisible #subtitleRoot .word,
|
|
||||||
body.layer-invisible #subtitleRoot .c {
|
|
||||||
color: transparent !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
-webkit-text-stroke: 0 !important;
|
|
||||||
-webkit-text-fill-color: transparent !important;
|
|
||||||
background: transparent !important;
|
|
||||||
caret-color: transparent !important;
|
|
||||||
line-height: var(--invisible-sub-line-height, normal) !important;
|
|
||||||
font-kerning: auto;
|
|
||||||
letter-spacing: normal;
|
|
||||||
font-variant-ligatures: normal;
|
|
||||||
font-feature-settings: normal;
|
|
||||||
text-rendering: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible #subtitleRoot br {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible #subtitleRoot .word:hover,
|
|
||||||
body.layer-invisible #subtitleRoot .c:hover,
|
|
||||||
body.layer-invisible #subtitleRoot.has-selection .word:hover,
|
|
||||||
body.layer-invisible #subtitleRoot.has-selection .c:hover {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible #subtitleRoot::selection,
|
|
||||||
body.layer-invisible #subtitleRoot .word::selection,
|
|
||||||
body.layer-invisible #subtitleRoot .c::selection {
|
|
||||||
background: transparent !important;
|
|
||||||
color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot,
|
|
||||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot .word,
|
|
||||||
body.layer-invisible.debug-invisible-visualization #subtitleRoot .c {
|
|
||||||
color: #ed8796 !important;
|
|
||||||
-webkit-text-fill-color: #ed8796 !important;
|
|
||||||
-webkit-text-stroke: var(--sub-border-size, 2px) rgba(0, 0, 0, 0.85) !important;
|
|
||||||
paint-order: stroke fill !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invisible-position-edit-hud {
|
|
||||||
position: absolute;
|
|
||||||
top: 14px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 30;
|
|
||||||
max-width: min(90vw, 1100px);
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.35;
|
|
||||||
color: rgba(255, 255, 255, 0.95);
|
|
||||||
background: rgba(22, 24, 36, 0.88);
|
|
||||||
border: 1px solid rgba(130, 150, 255, 0.55);
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible.invisible-position-edit .invisible-position-edit-hud {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-invisible.invisible-position-edit #subtitleRoot,
|
|
||||||
body.layer-invisible.invisible-position-edit #subtitleRoot .word,
|
|
||||||
body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
|
||||||
color: #ed8796 !important;
|
|
||||||
-webkit-text-fill-color: #ed8796 !important;
|
|
||||||
-webkit-text-stroke: var(--sub-border-size, 2px) rgba(0, 0, 0, 0.85) !important;
|
|
||||||
paint-order: stroke fill !important;
|
|
||||||
text-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#secondarySubContainer {
|
#secondarySubContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 40px;
|
top: 40px;
|
||||||
@@ -538,40 +555,6 @@ body.layer-invisible.invisible-position-edit #subtitleRoot .c {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layer-visible #secondarySubContainer,
|
|
||||||
body.layer-invisible #secondarySubContainer {
|
|
||||||
display: none !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-secondary #subtitleContainer,
|
|
||||||
body.layer-secondary .modal,
|
|
||||||
body.layer-secondary .overlay-error-toast {
|
|
||||||
display: none !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-secondary #overlay {
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-secondary #secondarySubContainer {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
transform: none;
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 8px 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.layer-modal #subtitleContainer,
|
body.layer-modal #subtitleContainer,
|
||||||
body.layer-modal #secondarySubContainer {
|
body.layer-modal #secondarySubContainer {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@@ -597,10 +580,6 @@ body.layer-modal #overlay {
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layer-secondary #secondarySubRoot {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#secondarySubRoot:empty {
|
#secondarySubRoot:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -644,11 +623,7 @@ body.settings-modal-open #secondarySubContainer {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.layer-secondary #secondarySubContainer.secondary-sub-hover {
|
iframe.yomitan-popup,
|
||||||
padding: 8px 12px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe[id^='yomitan-popup'] {
|
iframe[id^='yomitan-popup'] {
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
z-index: 2147483647 !important;
|
z-index: 2147483647 !important;
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
buildInvisibleTokenHoverRanges,
|
buildInvisibleTokenHoverRanges,
|
||||||
computeWordClass,
|
computeWordClass,
|
||||||
normalizeSubtitle,
|
normalizeSubtitle,
|
||||||
|
sanitizeSubtitleHoverTokenColor,
|
||||||
shouldRenderTokenizedSubtitle,
|
shouldRenderTokenizedSubtitle,
|
||||||
} from './subtitle-render.js';
|
} from './subtitle-render.js';
|
||||||
import { resolveInvisibleLineHeight } from './positioning/invisible-layout-helpers.js';
|
|
||||||
|
|
||||||
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
function createToken(overrides: Partial<MergedToken>): MergedToken {
|
||||||
return {
|
return {
|
||||||
@@ -210,6 +210,17 @@ test('computeWordClass skips frequency class when rank is out of topX', () => {
|
|||||||
assert.equal(actual, 'word');
|
assert.equal(actual, 'word');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sanitizeSubtitleHoverTokenColor falls back for pure black values', () => {
|
||||||
|
assert.equal(sanitizeSubtitleHoverTokenColor('#000000'), '#f4dbd6');
|
||||||
|
assert.equal(sanitizeSubtitleHoverTokenColor('000000'), '#f4dbd6');
|
||||||
|
assert.equal(sanitizeSubtitleHoverTokenColor('#0000'), '#f4dbd6');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizeSubtitleHoverTokenColor keeps non-black color values', () => {
|
||||||
|
assert.equal(sanitizeSubtitleHoverTokenColor('#ff00ff'), '#ff00ff');
|
||||||
|
assert.equal(sanitizeSubtitleHoverTokenColor(undefined), '#f4dbd6');
|
||||||
|
});
|
||||||
|
|
||||||
test('alignTokensToSourceText preserves newline separators between adjacent token surfaces', () => {
|
test('alignTokensToSourceText preserves newline separators between adjacent token surfaces', () => {
|
||||||
const tokens = [
|
const tokens = [
|
||||||
createToken({ surface: 'キリキリと', reading: 'きりきりと', headword: 'キリキリと' }),
|
createToken({ surface: 'キリキリと', reading: 'きりきりと', headword: 'キリキリと' }),
|
||||||
@@ -285,20 +296,16 @@ test('normalizeSubtitle collapses explicit line breaks when collapseLineBreaks i
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shouldRenderTokenizedSubtitle disables token rendering on invisible layer', () => {
|
test('shouldRenderTokenizedSubtitle enables token rendering when tokens exist', () => {
|
||||||
assert.equal(shouldRenderTokenizedSubtitle(true, 5), false);
|
assert.equal(shouldRenderTokenizedSubtitle(5), true);
|
||||||
});
|
assert.equal(shouldRenderTokenizedSubtitle(0), false);
|
||||||
|
|
||||||
test('shouldRenderTokenizedSubtitle enables token rendering on visible layer when tokens exist', () => {
|
|
||||||
assert.equal(shouldRenderTokenizedSubtitle(false, 5), true);
|
|
||||||
assert.equal(shouldRenderTokenizedSubtitle(false, 0), false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
||||||
const distCssPath = path.join(process.cwd(), 'dist', 'renderer', 'style.css');
|
const distCssPath = path.join(process.cwd(), 'dist', 'renderer', 'style.css');
|
||||||
const srcCssPath = path.join(process.cwd(), 'src', 'renderer', 'style.css');
|
const srcCssPath = path.join(process.cwd(), 'src', 'renderer', 'style.css');
|
||||||
|
|
||||||
const cssPath = fs.existsSync(distCssPath) ? distCssPath : srcCssPath;
|
const cssPath = fs.existsSync(srcCssPath) ? srcCssPath : distCssPath;
|
||||||
if (!fs.existsSync(cssPath)) {
|
if (!fs.existsSync(cssPath)) {
|
||||||
assert.fail(
|
assert.fail(
|
||||||
'JLPT CSS file missing. Run `bun run build` first, or ensure src/renderer/style.css exists.',
|
'JLPT CSS file missing. Run `bun run build` first, or ensure src/renderer/style.css exists.',
|
||||||
@@ -330,31 +337,86 @@ test('JLPT CSS rules use underline-only styling in renderer stylesheet', () => {
|
|||||||
assert.match(block, /color:\s*var\(/);
|
assert.match(block, /color:\s*var\(/);
|
||||||
}
|
}
|
||||||
|
|
||||||
const invisibleBlock = extractClassBlock(
|
|
||||||
cssText,
|
|
||||||
'body.layer-invisible #subtitleRoot',
|
|
||||||
);
|
|
||||||
assert.match(
|
|
||||||
invisibleBlock,
|
|
||||||
/line-height:\s*var\(--invisible-sub-line-height,\s*normal\)\s*!important;/,
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleMacBlock = extractClassBlock(
|
const visibleMacBlock = extractClassBlock(
|
||||||
cssText,
|
cssText,
|
||||||
'body.platform-macos.layer-visible #subtitleRoot',
|
'body.platform-macos.layer-visible #subtitleRoot',
|
||||||
);
|
);
|
||||||
assert.match(visibleMacBlock, /--visible-sub-line-height:\s*1\.64;/);
|
assert.match(visibleMacBlock, /--visible-sub-line-height:\s*1\.64;/);
|
||||||
assert.match(visibleMacBlock, /--visible-sub-line-gap:\s*0\.54em;/);
|
assert.match(visibleMacBlock, /--visible-sub-line-gap:\s*0\.54em;/);
|
||||||
});
|
|
||||||
|
|
||||||
test('invisible overlay uses looser line height on macOS for multi-line subtitles', () => {
|
const subtitleRootBlock = extractClassBlock(cssText, '#subtitleRoot');
|
||||||
assert.equal(resolveInvisibleLineHeight(1, true), '1.08');
|
assert.match(
|
||||||
assert.equal(resolveInvisibleLineHeight(2, true), '1.5');
|
subtitleRootBlock,
|
||||||
assert.equal(resolveInvisibleLineHeight(3, true), '1.62');
|
/--subtitle-hover-token-color:\s*#f4dbd6;/,
|
||||||
});
|
);
|
||||||
|
assert.match(
|
||||||
|
subtitleRootBlock,
|
||||||
|
/--subtitle-hover-token-background-color:\s*rgba\(54,\s*58,\s*79,\s*0\.84\);/,
|
||||||
|
);
|
||||||
|
assert.match(subtitleRootBlock, /-webkit-text-fill-color:\s*currentColor;/);
|
||||||
|
|
||||||
test('invisible overlay keeps default line height on non-macOS platforms', () => {
|
const charBlock = extractClassBlock(cssText, '#subtitleRoot .c');
|
||||||
assert.equal(resolveInvisibleLineHeight(1, false), 'normal');
|
assert.match(charBlock, /-webkit-text-fill-color:\s*currentColor\s*!important;/);
|
||||||
assert.equal(resolveInvisibleLineHeight(2, false), 'normal');
|
|
||||||
assert.equal(resolveInvisibleLineHeight(4, false), 'normal');
|
const wordBlock = extractClassBlock(cssText, '#subtitleRoot .word');
|
||||||
|
assert.match(wordBlock, /-webkit-text-fill-color:\s*currentColor\s*!important;/);
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
cssText,
|
||||||
|
/#subtitleRoot \.word:not\(\.word-known\):not\(\.word-n-plus-one\):not\(\.word-frequency-single\):not\(\s*\.word-frequency-band-1\s*\):not\(\.word-frequency-band-2\):not\(\.word-frequency-band-3\):not\(\.word-frequency-band-4\):not\(\s*\.word-frequency-band-5\s*\):hover\s*\{[\s\S]*?background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);[\s\S]*?color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;[\s\S]*?-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const coloredWordHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known:hover');
|
||||||
|
assert.match(
|
||||||
|
coloredWordHoverBlock,
|
||||||
|
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
|
||||||
|
);
|
||||||
|
assert.match(coloredWordHoverBlock, /border-radius:\s*3px;/);
|
||||||
|
assert.match(coloredWordHoverBlock, /font-weight:\s*800;/);
|
||||||
|
assert.doesNotMatch(coloredWordHoverBlock, /color:\s*var\(--subtitle-hover-token-color/);
|
||||||
|
assert.doesNotMatch(coloredWordHoverBlock, /-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color/);
|
||||||
|
|
||||||
|
const coloredWordSelectionBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known::selection');
|
||||||
|
assert.match(
|
||||||
|
coloredWordSelectionBlock,
|
||||||
|
/color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
coloredWordSelectionBlock,
|
||||||
|
/-webkit-text-fill-color:\s*var\(--subtitle-known-word-color,\s*#a6da95\)\s*!important;/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const coloredCharHoverBlock = extractClassBlock(cssText, '#subtitleRoot .word.word-known .c:hover');
|
||||||
|
assert.match(coloredCharHoverBlock, /background:\s*transparent;/);
|
||||||
|
assert.match(coloredCharHoverBlock, /color:\s*inherit\s*!important;/);
|
||||||
|
|
||||||
|
const selectionBlock = extractClassBlock(cssText, '#subtitleRoot::selection');
|
||||||
|
assert.match(
|
||||||
|
selectionBlock,
|
||||||
|
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\);/,
|
||||||
|
);
|
||||||
|
assert.match(selectionBlock, /color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/);
|
||||||
|
assert.match(
|
||||||
|
selectionBlock,
|
||||||
|
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const descendantSelectionBlock = extractClassBlock(cssText, '#subtitleRoot *::selection');
|
||||||
|
assert.match(
|
||||||
|
descendantSelectionBlock,
|
||||||
|
/background:\s*var\(--subtitle-hover-token-background-color,\s*rgba\(54,\s*58,\s*79,\s*0\.84\)\)\s*!important;/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
descendantSelectionBlock,
|
||||||
|
/color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
descendantSelectionBlock,
|
||||||
|
/-webkit-text-fill-color:\s*var\(--subtitle-hover-token-color,\s*#f4dbd6\)\s*!important;/,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.doesNotMatch(
|
||||||
|
cssText,
|
||||||
|
/body\.layer-visible\s+#secondarySubContainer\s*\{[^}]*display:\s*none/i,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,11 +15,8 @@ export type InvisibleTokenHoverRange = {
|
|||||||
tokenIndex: number;
|
tokenIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function shouldRenderTokenizedSubtitle(
|
export function shouldRenderTokenizedSubtitle(tokenCount: number): boolean {
|
||||||
isInvisibleLayer: boolean,
|
return tokenCount > 0;
|
||||||
tokenCount: number,
|
|
||||||
): boolean {
|
|
||||||
return !isInvisibleLayer && tokenCount > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWhitespaceOnly(value: string): boolean {
|
function isWhitespaceOnly(value: string): boolean {
|
||||||
@@ -47,6 +44,23 @@ function sanitizeHexColor(value: unknown, fallback: string): string {
|
|||||||
: fallback;
|
: fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeSubtitleHoverTokenColor(value: unknown): string {
|
||||||
|
const sanitized = sanitizeHexColor(value, '#f4dbd6');
|
||||||
|
const normalized = sanitized.replace(/^#/, '').toLowerCase();
|
||||||
|
if (normalized === '000' || normalized === '0000' || normalized === '000000' || normalized === '00000000') {
|
||||||
|
return '#f4dbd6';
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeSubtitleHoverTokenBackgroundColor(value: unknown): string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return 'rgba(54, 58, 79, 0.84)';
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed.length > 0 ? trimmed : 'rgba(54, 58, 79, 0.84)';
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
|
const DEFAULT_FREQUENCY_RENDER_SETTINGS: FrequencyRenderSettings = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
topX: 1000,
|
topX: 1000,
|
||||||
@@ -79,6 +93,54 @@ function sanitizeFrequencyBandedColors(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyInlineStyleDeclarations(
|
||||||
|
target: HTMLElement,
|
||||||
|
declarations: Record<string, unknown>,
|
||||||
|
excludedKeys: ReadonlySet<string> = new Set<string>(),
|
||||||
|
): void {
|
||||||
|
for (const [key, value] of Object.entries(declarations)) {
|
||||||
|
if (excludedKeys.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value === null || value === undefined || typeof value === 'object') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssValue = String(value);
|
||||||
|
if (key.startsWith('-') || key.includes('-')) {
|
||||||
|
target.style.setProperty(key, cssValue);
|
||||||
|
if (key === '--webkit-text-stroke') {
|
||||||
|
target.style.setProperty('-webkit-text-stroke', cssValue);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleTarget = target.style as unknown as Record<string, string>;
|
||||||
|
styleTarget[key] = cssValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickInlineStyleDeclarations(
|
||||||
|
declarations: Record<string, unknown>,
|
||||||
|
includedKeys: ReadonlySet<string>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const picked: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(declarations)) {
|
||||||
|
if (!includedKeys.has(key)) continue;
|
||||||
|
picked[key] = value;
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTAINER_STYLE_KEYS = new Set<string>([
|
||||||
|
'background',
|
||||||
|
'backgroundColor',
|
||||||
|
'backdropFilter',
|
||||||
|
'WebkitBackdropFilter',
|
||||||
|
'webkitBackdropFilter',
|
||||||
|
'-webkit-backdrop-filter',
|
||||||
|
]);
|
||||||
|
|
||||||
function getFrequencyDictionaryClass(
|
function getFrequencyDictionaryClass(
|
||||||
token: MergedToken,
|
token: MergedToken,
|
||||||
settings: FrequencyRenderSettings,
|
settings: FrequencyRenderSettings,
|
||||||
@@ -337,11 +399,8 @@ function renderPlainTextPreserveLineBreaks(root: ParentNode, text: string): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createSubtitleRenderer(ctx: RendererContext) {
|
export function createSubtitleRenderer(ctx: RendererContext) {
|
||||||
function renderSubtitle(data: SubtitleData | string): void {
|
function renderSubtitle(data: SubtitleData | string): void {
|
||||||
ctx.dom.subtitleRoot.innerHTML = '';
|
ctx.dom.subtitleRoot.innerHTML = '';
|
||||||
ctx.state.lastHoverSelectionKey = '';
|
|
||||||
ctx.state.lastHoverSelectionNode = null;
|
|
||||||
ctx.state.lastHoveredTokenIndex = null;
|
|
||||||
|
|
||||||
let text: string;
|
let text: string;
|
||||||
let tokens: MergedToken[] | null;
|
let tokens: MergedToken[] | null;
|
||||||
@@ -358,22 +417,8 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
|
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
if (ctx.platform.isInvisibleLayer) {
|
|
||||||
// Keep natural kerning/shaping in invisible layer to match mpv glyph placement.
|
|
||||||
const normalizedInvisible = normalizeSubtitle(text, false);
|
|
||||||
ctx.state.currentInvisibleSubtitleLineCount = Math.max(
|
|
||||||
1,
|
|
||||||
normalizedInvisible.split('\n').length,
|
|
||||||
);
|
|
||||||
ctx.state.invisibleTokenHoverSourceText = normalizedInvisible;
|
|
||||||
ctx.state.invisibleTokenHoverRanges =
|
|
||||||
tokens && tokens.length > 0 ? buildInvisibleTokenHoverRanges(tokens, normalizedInvisible) : [];
|
|
||||||
renderPlainTextPreserveLineBreaks(ctx.dom.subtitleRoot, normalizedInvisible);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = normalizeSubtitle(text, true, !ctx.state.preserveSubtitleLineBreaks);
|
const normalized = normalizeSubtitle(text, true, !ctx.state.preserveSubtitleLineBreaks);
|
||||||
if (shouldRenderTokenizedSubtitle(ctx.platform.isInvisibleLayer, tokens?.length ?? 0) && tokens) {
|
if (shouldRenderTokenizedSubtitle(tokens?.length ?? 0) && tokens) {
|
||||||
renderWithTokens(
|
renderWithTokens(
|
||||||
ctx.dom.subtitleRoot,
|
ctx.dom.subtitleRoot,
|
||||||
tokens,
|
tokens,
|
||||||
@@ -444,17 +489,30 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
function applySubtitleStyle(style: SubtitleStyleConfig | null): void {
|
function applySubtitleStyle(style: SubtitleStyleConfig | null): void {
|
||||||
if (!style) return;
|
if (!style) return;
|
||||||
|
|
||||||
|
const styleDeclarations = style as Record<string, unknown>;
|
||||||
|
applyInlineStyleDeclarations(
|
||||||
|
ctx.dom.subtitleRoot,
|
||||||
|
styleDeclarations,
|
||||||
|
CONTAINER_STYLE_KEYS,
|
||||||
|
);
|
||||||
|
applyInlineStyleDeclarations(
|
||||||
|
ctx.dom.subtitleContainer,
|
||||||
|
pickInlineStyleDeclarations(styleDeclarations, CONTAINER_STYLE_KEYS),
|
||||||
|
);
|
||||||
|
|
||||||
if (style.fontFamily) ctx.dom.subtitleRoot.style.fontFamily = style.fontFamily;
|
if (style.fontFamily) ctx.dom.subtitleRoot.style.fontFamily = style.fontFamily;
|
||||||
if (style.fontSize) ctx.dom.subtitleRoot.style.fontSize = `${style.fontSize}px`;
|
if (style.fontSize) ctx.dom.subtitleRoot.style.fontSize = `${style.fontSize}px`;
|
||||||
if (style.fontColor) ctx.dom.subtitleRoot.style.color = style.fontColor;
|
if (style.fontColor) {
|
||||||
|
ctx.dom.subtitleRoot.style.color = style.fontColor;
|
||||||
|
}
|
||||||
if (style.fontWeight) ctx.dom.subtitleRoot.style.fontWeight = style.fontWeight;
|
if (style.fontWeight) ctx.dom.subtitleRoot.style.fontWeight = style.fontWeight;
|
||||||
if (style.fontStyle) ctx.dom.subtitleRoot.style.fontStyle = style.fontStyle;
|
if (style.fontStyle) ctx.dom.subtitleRoot.style.fontStyle = style.fontStyle;
|
||||||
if (style.backgroundColor) {
|
|
||||||
ctx.dom.subtitleContainer.style.background = style.backgroundColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
const knownWordColor = style.knownWordColor ?? ctx.state.knownWordColor ?? '#a6da95';
|
||||||
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
const nPlusOneColor = style.nPlusOneColor ?? ctx.state.nPlusOneColor ?? '#c6a0f6';
|
||||||
|
const hoverTokenColor = sanitizeSubtitleHoverTokenColor(style.hoverTokenColor);
|
||||||
|
const hoverTokenBackgroundColor = sanitizeSubtitleHoverTokenBackgroundColor(
|
||||||
|
style.hoverTokenBackgroundColor,
|
||||||
|
);
|
||||||
const jlptColors = {
|
const jlptColors = {
|
||||||
N1: ctx.state.jlptN1Color ?? '#ed8796',
|
N1: ctx.state.jlptN1Color ?? '#ed8796',
|
||||||
N2: ctx.state.jlptN2Color ?? '#f5a97f',
|
N2: ctx.state.jlptN2Color ?? '#f5a97f',
|
||||||
@@ -476,6 +534,11 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
ctx.state.nPlusOneColor = nPlusOneColor;
|
ctx.state.nPlusOneColor = nPlusOneColor;
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-known-word-color', knownWordColor);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-known-word-color', knownWordColor);
|
||||||
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-n-plus-one-color', nPlusOneColor);
|
||||||
|
ctx.dom.subtitleRoot.style.setProperty('--subtitle-hover-token-color', hoverTokenColor);
|
||||||
|
ctx.dom.subtitleRoot.style.setProperty(
|
||||||
|
'--subtitle-hover-token-background-color',
|
||||||
|
hoverTokenBackgroundColor,
|
||||||
|
);
|
||||||
ctx.state.jlptN1Color = jlptColors.N1;
|
ctx.state.jlptN1Color = jlptColors.N1;
|
||||||
ctx.state.jlptN2Color = jlptColors.N2;
|
ctx.state.jlptN2Color = jlptColors.N2;
|
||||||
ctx.state.jlptN3Color = jlptColors.N3;
|
ctx.state.jlptN3Color = jlptColors.N3;
|
||||||
@@ -551,6 +614,17 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
const secondaryStyle = style.secondary;
|
const secondaryStyle = style.secondary;
|
||||||
if (!secondaryStyle) return;
|
if (!secondaryStyle) return;
|
||||||
|
|
||||||
|
const secondaryStyleDeclarations = secondaryStyle as Record<string, unknown>;
|
||||||
|
applyInlineStyleDeclarations(
|
||||||
|
ctx.dom.secondarySubRoot,
|
||||||
|
secondaryStyleDeclarations,
|
||||||
|
CONTAINER_STYLE_KEYS,
|
||||||
|
);
|
||||||
|
applyInlineStyleDeclarations(
|
||||||
|
ctx.dom.secondarySubContainer,
|
||||||
|
pickInlineStyleDeclarations(secondaryStyleDeclarations, CONTAINER_STYLE_KEYS),
|
||||||
|
);
|
||||||
|
|
||||||
if (secondaryStyle.fontFamily) {
|
if (secondaryStyle.fontFamily) {
|
||||||
ctx.dom.secondarySubRoot.style.fontFamily = secondaryStyle.fontFamily;
|
ctx.dom.secondarySubRoot.style.fontFamily = secondaryStyle.fontFamily;
|
||||||
}
|
}
|
||||||
@@ -566,9 +640,6 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
if (secondaryStyle.fontStyle) {
|
if (secondaryStyle.fontStyle) {
|
||||||
ctx.dom.secondarySubRoot.style.fontStyle = secondaryStyle.fontStyle;
|
ctx.dom.secondarySubRoot.style.fontStyle = secondaryStyle.fontStyle;
|
||||||
}
|
}
|
||||||
if (secondaryStyle.backgroundColor) {
|
|
||||||
ctx.dom.secondarySubContainer.style.background = secondaryStyle.backgroundColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,40 +1,25 @@
|
|||||||
export type OverlayLayer = 'visible' | 'invisible' | 'secondary' | 'modal';
|
export type OverlayLayer = 'visible' | 'modal';
|
||||||
|
|
||||||
export type PlatformInfo = {
|
export type PlatformInfo = {
|
||||||
overlayLayer: OverlayLayer;
|
overlayLayer: OverlayLayer;
|
||||||
isInvisibleLayer: boolean;
|
|
||||||
isSecondaryLayer: boolean;
|
|
||||||
isModalLayer: boolean;
|
isModalLayer: boolean;
|
||||||
isLinuxPlatform: boolean;
|
isLinuxPlatform: boolean;
|
||||||
isMacOSPlatform: boolean;
|
isMacOSPlatform: boolean;
|
||||||
shouldToggleMouseIgnore: boolean;
|
shouldToggleMouseIgnore: boolean;
|
||||||
invisiblePositionEditToggleCode: string;
|
|
||||||
invisiblePositionStepPx: number;
|
|
||||||
invisiblePositionStepFastPx: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolvePlatformInfo(): PlatformInfo {
|
export function resolvePlatformInfo(): PlatformInfo {
|
||||||
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
const overlayLayerFromPreload = window.electronAPI.getOverlayLayer();
|
||||||
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
const queryLayer = new URLSearchParams(window.location.search).get('layer');
|
||||||
const overlayLayerFromQuery: OverlayLayer | null =
|
const overlayLayerFromQuery: OverlayLayer | null =
|
||||||
queryLayer === 'visible' ||
|
queryLayer === 'visible' || queryLayer === 'modal' ? queryLayer : null;
|
||||||
queryLayer === 'invisible' ||
|
|
||||||
queryLayer === 'secondary' ||
|
|
||||||
queryLayer === 'modal'
|
|
||||||
? queryLayer
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const overlayLayer: OverlayLayer =
|
const overlayLayer: OverlayLayer =
|
||||||
overlayLayerFromQuery ??
|
overlayLayerFromQuery ??
|
||||||
(overlayLayerFromPreload === 'visible' ||
|
(overlayLayerFromPreload === 'visible' || overlayLayerFromPreload === 'modal'
|
||||||
overlayLayerFromPreload === 'invisible' ||
|
|
||||||
overlayLayerFromPreload === 'secondary' ||
|
|
||||||
overlayLayerFromPreload === 'modal'
|
|
||||||
? overlayLayerFromPreload
|
? overlayLayerFromPreload
|
||||||
: 'visible');
|
: 'visible');
|
||||||
|
|
||||||
const isInvisibleLayer = overlayLayer === 'invisible';
|
|
||||||
const isSecondaryLayer = overlayLayer === 'secondary';
|
|
||||||
const isModalLayer = overlayLayer === 'modal';
|
const isModalLayer = overlayLayer === 'modal';
|
||||||
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
const isLinuxPlatform = navigator.platform.toLowerCase().includes('linux');
|
||||||
const isMacOSPlatform =
|
const isMacOSPlatform =
|
||||||
@@ -42,14 +27,9 @@ export function resolvePlatformInfo(): PlatformInfo {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
overlayLayer,
|
overlayLayer,
|
||||||
isInvisibleLayer,
|
|
||||||
isSecondaryLayer,
|
|
||||||
isModalLayer,
|
isModalLayer,
|
||||||
isLinuxPlatform,
|
isLinuxPlatform,
|
||||||
isMacOSPlatform,
|
isMacOSPlatform,
|
||||||
shouldToggleMouseIgnore: !isLinuxPlatform && !isSecondaryLayer && !isModalLayer,
|
shouldToggleMouseIgnore: !isLinuxPlatform && !isModalLayer,
|
||||||
invisiblePositionEditToggleCode: 'KeyP',
|
|
||||||
invisiblePositionStepPx: 1,
|
|
||||||
invisiblePositionStepFastPx: 4,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,14 @@ export const IPC_CHANNELS = {
|
|||||||
refreshKnownWords: 'anki:refresh-known-words',
|
refreshKnownWords: 'anki:refresh-known-words',
|
||||||
kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
|
kikuFieldGroupingRespond: 'kiku:field-grouping-respond',
|
||||||
reportOverlayContentBounds: 'overlay-content-bounds:report',
|
reportOverlayContentBounds: 'overlay-content-bounds:report',
|
||||||
|
overlayModalOpened: 'overlay:modal-opened',
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
getOverlayVisibility: 'get-overlay-visibility',
|
getOverlayVisibility: 'get-overlay-visibility',
|
||||||
getVisibleOverlayVisibility: 'get-visible-overlay-visibility',
|
getVisibleOverlayVisibility: 'get-visible-overlay-visibility',
|
||||||
getInvisibleOverlayVisibility: 'get-invisible-overlay-visibility',
|
|
||||||
getCurrentSubtitle: 'get-current-subtitle',
|
getCurrentSubtitle: 'get-current-subtitle',
|
||||||
getCurrentSubtitleRaw: 'get-current-subtitle-raw',
|
getCurrentSubtitleRaw: 'get-current-subtitle-raw',
|
||||||
getCurrentSubtitleAss: 'get-current-subtitle-ass',
|
getCurrentSubtitleAss: 'get-current-subtitle-ass',
|
||||||
getMpvSubtitleRenderMetrics: 'get-mpv-subtitle-render-metrics',
|
|
||||||
getSubtitlePosition: 'get-subtitle-position',
|
getSubtitlePosition: 'get-subtitle-position',
|
||||||
getSubtitleStyle: 'get-subtitle-style',
|
getSubtitleStyle: 'get-subtitle-style',
|
||||||
getMecabStatus: 'get-mecab-status',
|
getMecabStatus: 'get-mecab-status',
|
||||||
@@ -57,9 +56,7 @@ export const IPC_CHANNELS = {
|
|||||||
subtitleSet: 'subtitle:set',
|
subtitleSet: 'subtitle:set',
|
||||||
subtitleVisibility: 'mpv:subVisibility',
|
subtitleVisibility: 'mpv:subVisibility',
|
||||||
subtitlePositionSet: 'subtitle-position:set',
|
subtitlePositionSet: 'subtitle-position:set',
|
||||||
mpvSubtitleRenderMetricsSet: 'mpv-subtitle-render-metrics:set',
|
|
||||||
subtitleAssSet: 'subtitle-ass:set',
|
subtitleAssSet: 'subtitle-ass:set',
|
||||||
overlayDebugVisualizationSet: 'overlay-debug-visualization:set',
|
|
||||||
secondarySubtitleSet: 'secondary-subtitle:set',
|
secondarySubtitleSet: 'secondary-subtitle:set',
|
||||||
secondarySubtitleMode: 'secondary-subtitle:mode',
|
secondarySubtitleMode: 'secondary-subtitle:mode',
|
||||||
subsyncOpenManual: 'subsync:open-manual',
|
subsyncOpenManual: 'subsync:open-manual',
|
||||||
|
|||||||
19
src/types.ts
19
src/types.ts
@@ -71,8 +71,6 @@ export interface WindowGeometry {
|
|||||||
|
|
||||||
export interface SubtitlePosition {
|
export interface SubtitlePosition {
|
||||||
yPercent: number;
|
yPercent: number;
|
||||||
invisibleOffsetXPx?: number;
|
|
||||||
invisibleOffsetYPx?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtitleStyle {
|
export interface SubtitleStyle {
|
||||||
@@ -272,6 +270,7 @@ export interface SubtitleStyleConfig {
|
|||||||
enableJlpt?: boolean;
|
enableJlpt?: boolean;
|
||||||
preserveLineBreaks?: boolean;
|
preserveLineBreaks?: boolean;
|
||||||
hoverTokenColor?: string;
|
hoverTokenColor?: string;
|
||||||
|
hoverTokenBackgroundColor?: string;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
fontColor?: string;
|
fontColor?: string;
|
||||||
@@ -309,7 +308,6 @@ export type FrequencyDictionaryMode = 'single' | 'banded';
|
|||||||
|
|
||||||
export interface ShortcutsConfig {
|
export interface ShortcutsConfig {
|
||||||
toggleVisibleOverlayGlobal?: string | null;
|
toggleVisibleOverlayGlobal?: string | null;
|
||||||
toggleInvisibleOverlayGlobal?: string | null;
|
|
||||||
copySubtitle?: string | null;
|
copySubtitle?: string | null;
|
||||||
copySubtitleMultiple?: string | null;
|
copySubtitleMultiple?: string | null;
|
||||||
updateLastCardFromClipboard?: string | null;
|
updateLastCardFromClipboard?: string | null;
|
||||||
@@ -364,10 +362,6 @@ export interface DiscordPresenceConfig {
|
|||||||
debounceMs?: number;
|
debounceMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InvisibleOverlayConfig {
|
|
||||||
startupVisibility?: 'platform-default' | 'visible' | 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type YoutubeSubgenMode = 'automatic' | 'preprocess' | 'off';
|
export type YoutubeSubgenMode = 'automatic' | 'preprocess' | 'off';
|
||||||
|
|
||||||
export interface YoutubeSubgenConfig {
|
export interface YoutubeSubgenConfig {
|
||||||
@@ -410,7 +404,6 @@ export interface Config {
|
|||||||
anilist?: AnilistConfig;
|
anilist?: AnilistConfig;
|
||||||
jellyfin?: JellyfinConfig;
|
jellyfin?: JellyfinConfig;
|
||||||
discordPresence?: DiscordPresenceConfig;
|
discordPresence?: DiscordPresenceConfig;
|
||||||
invisibleOverlay?: InvisibleOverlayConfig;
|
|
||||||
youtubeSubgen?: YoutubeSubgenConfig;
|
youtubeSubgen?: YoutubeSubgenConfig;
|
||||||
immersionTracking?: ImmersionTrackingConfig;
|
immersionTracking?: ImmersionTrackingConfig;
|
||||||
logging?: {
|
logging?: {
|
||||||
@@ -540,7 +533,6 @@ export interface ResolvedConfig {
|
|||||||
updateIntervalMs: number;
|
updateIntervalMs: number;
|
||||||
debounceMs: number;
|
debounceMs: number;
|
||||||
};
|
};
|
||||||
invisibleOverlay: Required<InvisibleOverlayConfig>;
|
|
||||||
youtubeSubgen: YoutubeSubgenConfig & {
|
youtubeSubgen: YoutubeSubgenConfig & {
|
||||||
mode: YoutubeSubgenMode;
|
mode: YoutubeSubgenMode;
|
||||||
whisperBin: string;
|
whisperBin: string;
|
||||||
@@ -630,7 +622,7 @@ export interface MpvSubtitleRenderMetrics {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OverlayLayer = 'visible' | 'invisible';
|
export type OverlayLayer = 'visible';
|
||||||
|
|
||||||
export interface OverlayContentRect {
|
export interface OverlayContentRect {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -728,7 +720,7 @@ export interface SubtitleHoverTokenPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
getOverlayLayer: () => 'visible' | 'invisible' | 'secondary' | 'modal' | null;
|
getOverlayLayer: () => 'visible' | 'modal' | null;
|
||||||
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
onSubtitle: (callback: (data: SubtitleData) => void) => void;
|
||||||
onVisibility: (callback: (visible: boolean) => void) => void;
|
onVisibility: (callback: (visible: boolean) => void) => void;
|
||||||
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
onSubtitlePosition: (callback: (position: SubtitlePosition | null) => void) => void;
|
||||||
@@ -736,10 +728,7 @@ export interface ElectronAPI {
|
|||||||
getCurrentSubtitle: () => Promise<SubtitleData>;
|
getCurrentSubtitle: () => Promise<SubtitleData>;
|
||||||
getCurrentSubtitleRaw: () => Promise<string>;
|
getCurrentSubtitleRaw: () => Promise<string>;
|
||||||
getCurrentSubtitleAss: () => Promise<string>;
|
getCurrentSubtitleAss: () => Promise<string>;
|
||||||
getMpvSubtitleRenderMetrics: () => Promise<MpvSubtitleRenderMetrics>;
|
|
||||||
onMpvSubtitleRenderMetrics: (callback: (metrics: MpvSubtitleRenderMetrics) => void) => void;
|
|
||||||
onSubtitleAss: (callback: (assText: string) => void) => void;
|
onSubtitleAss: (callback: (assText: string) => void) => void;
|
||||||
onOverlayDebugVisualization: (callback: (enabled: boolean) => void) => void;
|
|
||||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||||
openYomitanSettings: () => void;
|
openYomitanSettings: () => void;
|
||||||
getSubtitlePosition: () => Promise<SubtitlePosition | null>;
|
getSubtitlePosition: () => Promise<SubtitlePosition | null>;
|
||||||
@@ -781,8 +770,8 @@ export interface ElectronAPI {
|
|||||||
onOpenJimaku: (callback: () => void) => void;
|
onOpenJimaku: (callback: () => void) => void;
|
||||||
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
appendClipboardVideoToQueue: () => Promise<ClipboardAppendResult>;
|
||||||
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
notifyOverlayModalClosed: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
||||||
|
notifyOverlayModalOpened: (modal: 'runtime-options' | 'subsync' | 'jimaku' | 'kiku') => void;
|
||||||
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
reportOverlayContentBounds: (measurement: OverlayContentMeasurement) => void;
|
||||||
reportHoveredSubtitleToken: (tokenIndex: number | null) => void;
|
|
||||||
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
onConfigHotReload: (callback: (payload: ConfigHotReloadPayload) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user