feat: bind overlay state to secondary subtitle mpv visibility

This commit is contained in:
2026-02-26 16:40:51 -08:00
parent 74554a30f0
commit 75442a4648
48 changed files with 1231 additions and 1070 deletions

View File

@@ -78,7 +78,7 @@ src/
### 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`
- **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`
@@ -102,7 +102,6 @@ src/renderer/
positioning.ts # Facade export for positioning controller
positioning/
controller.ts # Position controller orchestration
invisible-layout*.ts # Invisible layer layout computations
position-state.ts # Position state helpers
handlers/
keyboard.ts # Keybindings, chord handling, modal key routing
@@ -125,7 +124,7 @@ src/renderer/
## 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
flowchart LR
@@ -162,7 +161,7 @@ flowchart LR
subgraph Svc["Services — src/core/services/"]
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
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc
Tracking["Tracking<br/>anilist · jellyfin<br/>immersion · discord"]:::svc
@@ -172,9 +171,7 @@ flowchart LR
Bridge(["preload.ts<br/>Electron IPC"]):::bridge
subgraph Rend["Renderer — src/renderer/"]
Visible["Visible window<br/>Yomitan lookups"]:::rend
Invisible["Invisible window<br/>mpv positioning"]:::rend
Secondary["Secondary window<br/>subtitle bar"]:::rend
Overlay["Main overlay window<br/>primary + secondary subtitles"]:::rend
UI["subtitle-render<br/>positioning<br/>handlers · modals"]:::rend
end
@@ -193,10 +190,8 @@ flowchart LR
DiscordExt <-->|"RPC"| Integrations
Overlay & Mining --> Bridge
Bridge --> Visible
Bridge --> Invisible
Bridge --> Secondary
Visible & Invisible & Secondary --> UI
Bridge --> Overlay
Overlay --> UI
style Comp 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.
- **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`.
- **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.
- **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.
```mermaid
@@ -298,14 +293,10 @@ flowchart LR
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
OverlayInit --> VisWin["Visible window<br/>Yomitan lookups"]:::init
OverlayInit --> InvWin["Invisible window<br/>mpv positioning"]:::init
OverlayInit --> SecWin["Secondary window<br/>subtitle bar"]:::init
OverlayInit --> MainWin["Main overlay window<br/>primary + secondary subtitles"]:::init
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
VisWin --> Warmups
InvWin --> Warmups
SecWin --> Warmups
MainWin --> Warmups
Shortcuts --> Warmups
Warmups["Background<br/>warmups"]:::phase
@@ -330,7 +321,7 @@ flowchart LR
ExtEvt["Shortcuts · config hot-reload"]:::runtime
MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::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
WarmupGroup --> Loop

View File

@@ -38,8 +38,8 @@ features:
- icon:
src: /assets/dual-layer.svg
alt: Dual layer icon
title: Three-Plane Overlay Stack
details: Secondary context plane + visible interactive layer + invisible interaction plane, each with independent behavior and startup state.
title: Unified Overlay Stack
details: Primary interactive subtitle layer with a built-in secondary context bar, all in one overlay window.
- icon:
src: /assets/highlight.svg
alt: Highlight icon

View File

@@ -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.
## 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:
@@ -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
- **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.
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.
Jimaku search, field-grouping, runtime options, and manual subsync open as modal surfaces on top of the same overlay window.
## Looking Up Words
@@ -73,10 +59,10 @@ Toggle controls:
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.
### On the Invisible Overlay
### On Overlay Subtitles
1. The invisible layer sits over mpv's own subtitle text.
2. Click on any word in the subtitle — SubMiner maps your click position to the underlying text.
1. Subtitles are rendered directly in the overlay.
2. Click on any word in the subtitle.
3. On macOS, word selection happens automatically on hover.
4. Yomitan popup appears for lookup and card creation.

View 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.