mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 08:12:54 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6af9a19ff3
|
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
id: TASK-357
|
||||||
|
title: Add primary subtitle bar visibility modes
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- Codex
|
||||||
|
created_date: '2026-05-13 03:17'
|
||||||
|
updated_date: '2026-05-13 03:24'
|
||||||
|
labels:
|
||||||
|
- overlay
|
||||||
|
- subtitles
|
||||||
|
- config
|
||||||
|
dependencies: []
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Update the primary subtitle bar visibility control so the `v` key cycles through the same mode model used by secondary subtitles: hidden, visible, and hover/auto. The primary and secondary subtitle bars must keep separate visibility state and config defaults, and primary visibility changes must identify the primary bar in OSD feedback.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 Pressing `v` cycles primary subtitle bar visibility through hidden, visible, and hover/auto without changing secondary subtitle visibility.
|
||||||
|
- [x] #2 In hover/auto mode, the primary subtitle bar is normally hidden but becomes visible and interactable when hovering over its reserved location.
|
||||||
|
- [x] #3 Primary and secondary subtitle visibility modes have independent runtime state and config defaults.
|
||||||
|
- [x] #4 `subtitleStyle` exposes a primary subtitle visibility default that defaults to visible and validates invalid values with a warning/fallback.
|
||||||
|
- [x] #5 OSD feedback is shown when primary visibility mode changes and clearly identifies the primary subtitle bar.
|
||||||
|
- [x] #6 Relevant tests and config example/docs are updated.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
1. Add failing tests for primary subtitle mode config default/validation and renderer `v` cycling/OSD behavior.
|
||||||
|
2. Add primary subtitle mode type/config default under `subtitleStyle`, parse it with warning fallback, include it in generated example config.
|
||||||
|
3. Carry primary mode in renderer state/startup/hot-reload payloads independently from secondary mode.
|
||||||
|
4. Replace primary boolean toggle with `hidden -> visible -> hover` cycle and OSD text `Primary subtitle: <mode>`.
|
||||||
|
5. Add primary hover CSS mirroring secondary hover behavior so hover mode reserves a hit area and becomes interactable on hover.
|
||||||
|
6. Run focused tests, then broader relevant checks if time/environment allows.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Implemented primary subtitle visibility as independent `PrimarySubMode` (`hidden | visible | hover`) with renderer state, startup default from `subtitleStyle.primaryDefaultMode`, hot-reload payload propagation, and primary-specific OSD. Focused tests were added before implementation and all focused tests passed. Full relevant gate passed: `bun run typecheck`, `bun run test:config`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`, `bun run docs:test`, `bun run docs:build`, `bun run format:check:src`, and `bun run changelog:lint`.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Summary:
|
||||||
|
- Added `subtitleStyle.primaryDefaultMode` with default `visible`, enum validation, generated config examples, and renderer/hot-reload payload wiring.
|
||||||
|
- Changed the primary subtitle bar `v` action from a boolean hide/show toggle to independent `hidden | visible | hover` mode cycling with OSD text that identifies the primary subtitle.
|
||||||
|
- Added hover-mode CSS so the primary bar stays invisible until its location is hovered, then becomes visible and interactable.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- `bun test src/config/resolve/subtitle-style.test.ts src/config/config.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/subtitle-render.test.ts src/main/runtime/config-hot-reload-handlers.test.ts`
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bun run test:config`
|
||||||
|
- `bun run test:fast`
|
||||||
|
- `bun run test:env`
|
||||||
|
- `bun run build`
|
||||||
|
- `bun run test:smoke:dist`
|
||||||
|
- `bun run docs:test`
|
||||||
|
- `bun run docs:build`
|
||||||
|
- `bun run format:check:src`
|
||||||
|
- `bun run changelog:lint`
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
id: TASK-358
|
|
||||||
title: Show and verify default keybindings in example config
|
|
||||||
status: Done
|
|
||||||
assignee:
|
|
||||||
- '@Codex'
|
|
||||||
created_date: '2026-05-13 03:33'
|
|
||||||
updated_date: '2026-05-13 03:45'
|
|
||||||
labels:
|
|
||||||
- config
|
|
||||||
- keybindings
|
|
||||||
- overlay
|
|
||||||
- mpv
|
|
||||||
dependencies: []
|
|
||||||
priority: medium
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- SECTION:DESCRIPTION:BEGIN -->
|
|
||||||
Keep the shipped example configuration, overlay runtime, and mpv plugin aligned with the built-in default keybindings. The example `keybindings` array should show the same defaults that are active by default, and focused tests should catch drift between documented defaults and actual overlay/mpv wiring.
|
|
||||||
<!-- SECTION:DESCRIPTION:END -->
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
<!-- AC:BEGIN -->
|
|
||||||
- [x] #1 `config.example.jsonc` and the docs-site copy show all default keybindings in the `keybindings` array.
|
|
||||||
- [x] #2 Default keybindings are registered without conflicts in the overlay session-binding path.
|
|
||||||
- [x] #3 Default keybindings are registered and dispatched correctly inside the mpv plugin.
|
|
||||||
- [x] #4 Focused regression tests cover default keybinding/config-example parity and mpv/plugin dispatch.
|
|
||||||
<!-- AC:END -->
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
<!-- SECTION:PLAN:BEGIN -->
|
|
||||||
1. Inspect default keybinding definitions, config-example generation, overlay shortcut/session-binding tests, and mpv plugin binding tests.
|
|
||||||
2. Add failing tests for config-example keybinding parity and any missing default overlay/mpv wiring.
|
|
||||||
3. Update generated/example config and source wiring only where tests show drift.
|
|
||||||
4. Run focused Bun/Lua tests, regenerate examples if needed, update task AC/final notes.
|
|
||||||
<!-- SECTION:PLAN:END -->
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
|
|
||||||
<!-- SECTION:NOTES:BEGIN -->
|
|
||||||
Implemented the config-template path by injecting `DEFAULT_KEYBINDINGS` into generated examples when the resolved config has an empty `keybindings` array, preserving runtime merge semantics. Added coverage for template parity, default binding compile/action mapping, overlay keyboard dispatch, and mpv plugin registration/dispatch. Regenerated both `config.example.jsonc` artifacts and added changelog fragment `changes/358-default-keybindings-config-example.md`.
|
|
||||||
<!-- SECTION:NOTES:END -->
|
|
||||||
|
|
||||||
## Final Summary
|
|
||||||
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
|
||||||
Updated generated example configuration so `config.example.jsonc` and `docs-site/public/config.example.jsonc` now show every built-in default keybinding in the `keybindings` array instead of `[]`. The template copy now describes the array as default plus custom keybindings, while runtime default merge behavior remains unchanged.
|
|
||||||
|
|
||||||
Added regression coverage that the generated template parses back to `DEFAULT_KEYBINDINGS`, that every default binding compiles to the expected mpv command or session action, that the overlay keyboard handler dispatches all compiled defaults, and that the mpv plugin registers and invokes default mpv/session-action bindings. Also updated docs tables to include the default fullscreen binding and clarified that keybindings can target mpv commands or SubMiner session actions.
|
|
||||||
|
|
||||||
Verification passed: `bun run format:check:src`, `bun run changelog:lint`, `bun run docs:test`, `bun run docs:build`, `bun run verify:config-example`, focused config/session/renderer/plugin tests, `bun run typecheck`, `bun run test:env`, `bun run test:fast`, `bun run build`, and `bun run test:smoke:dist`.
|
|
||||||
<!-- SECTION:FINAL_SUMMARY:END -->
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Overlay: Changed `v` to cycle the primary subtitle bar through visible, hover, and hidden modes with primary-specific OSD feedback, and added `subtitleStyle.primaryDefaultMode` to configure the startup default independently from secondary subtitles.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
type: changed
|
|
||||||
area: config
|
|
||||||
|
|
||||||
- Config: Expanded the generated example config so `keybindings` lists every built-in default and added regression coverage that those defaults compile, dispatch in the overlay, and register through the mpv plugin.
|
|
||||||
+3
-121
@@ -183,130 +183,11 @@
|
|||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Default and custom keybindings that are merged with built-in defaults.
|
// Extra keybindings that are merged with built-in defaults.
|
||||||
// Set command to null to disable a default keybinding.
|
// Set command to null to disable a default keybinding.
|
||||||
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"keybindings": [
|
"keybindings": [], // Extra keybindings that are merged with built-in defaults.
|
||||||
{
|
|
||||||
"key": "Space", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"pause"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyF", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"fullscreen"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyJ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"sid"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyJ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"secondary-sid"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowRight", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
5
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowLeft", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
-5
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowUp", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
60
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowDown", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
-60
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyH", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"sub-seek",
|
|
||||||
-1
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyL", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"sub-seek",
|
|
||||||
1
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+BracketRight", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__sub-delay-next-line"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+BracketLeft", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__sub-delay-prev-line"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Alt+KeyC", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__youtube-picker-open"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Alt+KeyP", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__playlist-browser-open"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Shift+KeyH", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__replay-subtitle"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Shift+KeyL", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__play-next-subtitle"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyQ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"quit"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+KeyW", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"quit"
|
|
||||||
] // Command setting.
|
|
||||||
}
|
|
||||||
], // Default and custom keybindings that are merged with built-in defaults.
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Secondary Subtitles
|
// Secondary Subtitles
|
||||||
@@ -346,6 +227,7 @@
|
|||||||
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
|
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
||||||
"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
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ See `config.example.jsonc` for detailed configuration options.
|
|||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
Add a `keybindings` array to configure keyboard shortcuts that send mpv commands or SubMiner session actions:
|
Add a `keybindings` array to configure keyboard shortcuts that send commands to mpv:
|
||||||
|
|
||||||
See `config.example.jsonc` for detailed configuration options and more examples.
|
See `config.example.jsonc` for detailed configuration options and more examples.
|
||||||
|
|
||||||
@@ -470,7 +470,6 @@ See `config.example.jsonc` for detailed configuration options and more examples.
|
|||||||
| Key | Command | Description |
|
| Key | Command | Description |
|
||||||
| -------------------- | ----------------------------- | --------------------------------------- |
|
| -------------------- | ----------------------------- | --------------------------------------- |
|
||||||
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
| `Space` | `["cycle", "pause"]` | Toggle pause |
|
||||||
| `KeyF` | `["cycle", "fullscreen"]` | Toggle fullscreen |
|
|
||||||
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
|
| `KeyJ` | `["cycle", "sid"]` | Cycle primary subtitle track |
|
||||||
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
|
| `Shift+KeyJ` | `["cycle", "secondary-sid"]` | Cycle secondary subtitle track |
|
||||||
| `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser |
|
| `Ctrl+Alt+KeyP` | `["__playlist-browser-open"]` | Open playlist browser |
|
||||||
|
|||||||
@@ -183,130 +183,11 @@
|
|||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Keybindings (MPV Commands)
|
// Keybindings (MPV Commands)
|
||||||
// Default and custom keybindings that are merged with built-in defaults.
|
// Extra keybindings that are merged with built-in defaults.
|
||||||
// Set command to null to disable a default keybinding.
|
// Set command to null to disable a default keybinding.
|
||||||
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
// Hot-reload: keybinding changes apply live and update the session help modal on reopen.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"keybindings": [
|
"keybindings": [], // Extra keybindings that are merged with built-in defaults.
|
||||||
{
|
|
||||||
"key": "Space", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"pause"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyF", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"fullscreen"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyJ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"sid"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyJ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"cycle",
|
|
||||||
"secondary-sid"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowRight", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
5
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowLeft", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
-5
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowUp", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
60
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "ArrowDown", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"seek",
|
|
||||||
-60
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyH", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"sub-seek",
|
|
||||||
-1
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+KeyL", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"sub-seek",
|
|
||||||
1
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+BracketRight", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__sub-delay-next-line"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Shift+BracketLeft", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__sub-delay-prev-line"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Alt+KeyC", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__youtube-picker-open"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Alt+KeyP", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__playlist-browser-open"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Shift+KeyH", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__replay-subtitle"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+Shift+KeyL", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"__play-next-subtitle"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "KeyQ", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"quit"
|
|
||||||
] // Command setting.
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Ctrl+KeyW", // Key setting.
|
|
||||||
"command": [
|
|
||||||
"quit"
|
|
||||||
] // Command setting.
|
|
||||||
}
|
|
||||||
], // Default and custom keybindings that are merged with built-in defaults.
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Secondary Subtitles
|
// Secondary Subtitles
|
||||||
@@ -346,6 +227,7 @@
|
|||||||
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
// Hot-reload: subtitle style changes apply live without restarting SubMiner.
|
||||||
// ==========================================
|
// ==========================================
|
||||||
"subtitleStyle": {
|
"subtitleStyle": {
|
||||||
|
"primaryDefaultMode": "visible", // Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover. Values: hidden | visible | hover
|
||||||
"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
|
||||||
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
"autoPauseVideoOnHover": true, // Automatically pause mpv playback while hovering subtitle text, then resume on leave. Values: true | false
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ These control playback and subtitle display. They require overlay window focus.
|
|||||||
| Shortcut | Action |
|
| Shortcut | Action |
|
||||||
| -------------------- | --------------------------------------------------- |
|
| -------------------- | --------------------------------------------------- |
|
||||||
| `Space` | Toggle mpv pause |
|
| `Space` | Toggle mpv pause |
|
||||||
| `F` | Toggle fullscreen |
|
|
||||||
| `V` | Toggle primary subtitle bar visibility |
|
| `V` | Toggle primary subtitle bar visibility |
|
||||||
| `J` | Cycle primary subtitle track |
|
| `J` | Cycle primary subtitle track |
|
||||||
| `Shift+J` | Cycle secondary subtitle track |
|
| `Shift+J` | Cycle secondary subtitle track |
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ local recorded = {
|
|||||||
bindings = {},
|
bindings = {},
|
||||||
removed = {},
|
removed = {},
|
||||||
async_calls = {},
|
async_calls = {},
|
||||||
mpv_commands = {},
|
|
||||||
osd = {},
|
osd = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +38,6 @@ function mp.remove_key_binding(name)
|
|||||||
recorded.removed[#recorded.removed + 1] = name
|
recorded.removed[#recorded.removed + 1] = name
|
||||||
end
|
end
|
||||||
|
|
||||||
function mp.commandv(...)
|
|
||||||
recorded.mpv_commands[#recorded.mpv_commands + 1] = { ... }
|
|
||||||
end
|
|
||||||
|
|
||||||
function mp.add_timeout(seconds, callback)
|
function mp.add_timeout(seconds, callback)
|
||||||
return {
|
return {
|
||||||
seconds = seconds,
|
seconds = seconds,
|
||||||
@@ -76,126 +71,6 @@ local ctx = {
|
|||||||
actionType = "session-action",
|
actionType = "session-action",
|
||||||
actionId = "mineSentenceMultiple",
|
actionId = "mineSentenceMultiple",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "Space",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "cycle", "pause" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyF",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "cycle", "fullscreen" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyJ",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "cycle", "sid" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyJ",
|
|
||||||
modifiers = { "shift" },
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "cycle", "secondary-sid" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "ArrowRight",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "seek", 5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "ArrowLeft",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "seek", -5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "ArrowUp",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "seek", 60 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "ArrowDown",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "seek", -60 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyH",
|
|
||||||
modifiers = { "shift" },
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "sub-seek", -1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyL",
|
|
||||||
modifiers = { "shift" },
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "sub-seek", 1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "BracketRight",
|
|
||||||
modifiers = { "shift" },
|
|
||||||
},
|
|
||||||
actionType = "session-action",
|
|
||||||
actionId = "shiftSubDelayNextLine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "BracketLeft",
|
|
||||||
modifiers = { "shift" },
|
|
||||||
},
|
|
||||||
actionType = "session-action",
|
|
||||||
actionId = "shiftSubDelayPrevLine",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyC",
|
|
||||||
modifiers = { "ctrl", "alt" },
|
|
||||||
},
|
|
||||||
actionType = "session-action",
|
|
||||||
actionId = "openYoutubePicker",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyP",
|
|
||||||
modifiers = { "ctrl", "alt" },
|
|
||||||
},
|
|
||||||
actionType = "session-action",
|
|
||||||
actionId = "openPlaylistBrowser",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyH",
|
|
||||||
modifiers = { "ctrl", "shift" },
|
|
||||||
},
|
|
||||||
actionType = "session-action",
|
|
||||||
actionId = "replayCurrentSubtitle",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key = {
|
key = {
|
||||||
code = "KeyL",
|
code = "KeyL",
|
||||||
@@ -204,22 +79,6 @@ local ctx = {
|
|||||||
actionType = "session-action",
|
actionType = "session-action",
|
||||||
actionId = "playNextSubtitle",
|
actionId = "playNextSubtitle",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyQ",
|
|
||||||
modifiers = {},
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "quit" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key = {
|
|
||||||
code = "KeyW",
|
|
||||||
modifiers = { "ctrl" },
|
|
||||||
},
|
|
||||||
actionType = "mpv-command",
|
|
||||||
command = { "quit" },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key = {
|
key = {
|
||||||
code = "KeyA",
|
code = "KeyA",
|
||||||
@@ -228,6 +87,14 @@ local ctx = {
|
|||||||
actionType = "session-action",
|
actionType = "session-action",
|
||||||
actionId = "openCharacterDictionary",
|
actionId = "openCharacterDictionary",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key = {
|
||||||
|
code = "KeyL",
|
||||||
|
modifiers = { "shift" },
|
||||||
|
},
|
||||||
|
actionType = "mpv-command",
|
||||||
|
command = { "sub-seek", 1 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
end,
|
end,
|
||||||
@@ -262,66 +129,31 @@ local ctx = {
|
|||||||
local bindings = session_bindings.create(ctx)
|
local bindings = session_bindings.create(ctx)
|
||||||
assert_true(bindings.register_bindings(), "session bindings should register")
|
assert_true(bindings.register_bindings(), "session bindings should register")
|
||||||
|
|
||||||
local function find_binding(keys)
|
local starter = nil
|
||||||
for _, binding in ipairs(recorded.bindings) do
|
for _, binding in ipairs(recorded.bindings) do
|
||||||
if binding.keys == keys then
|
if binding.keys == "Ctrl+S" then
|
||||||
return binding
|
starter = binding
|
||||||
end
|
break
|
||||||
end
|
end
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local starter = find_binding("Ctrl+S")
|
|
||||||
assert_true(starter ~= nil, "multi-mine starter binding should be registered")
|
assert_true(starter ~= nil, "multi-mine starter binding should be registered")
|
||||||
|
|
||||||
local expected_mpv_bindings = {
|
local play_next = nil
|
||||||
{ keys = "SPACE", command = { "cycle", "pause" } },
|
for _, binding in ipairs(recorded.bindings) do
|
||||||
{ keys = "f", command = { "cycle", "fullscreen" } },
|
if binding.keys == "Ctrl+L" then
|
||||||
{ keys = "j", command = { "cycle", "sid" } },
|
play_next = binding
|
||||||
{ keys = "J", command = { "cycle", "secondary-sid" } },
|
break
|
||||||
{ keys = "RIGHT", command = { "seek", 5 } },
|
|
||||||
{ keys = "LEFT", command = { "seek", -5 } },
|
|
||||||
{ keys = "UP", command = { "seek", 60 } },
|
|
||||||
{ keys = "DOWN", command = { "seek", -60 } },
|
|
||||||
{ keys = "H", command = { "sub-seek", -1 } },
|
|
||||||
{ keys = "L", command = { "sub-seek", 1 } },
|
|
||||||
{ keys = "q", command = { "quit" } },
|
|
||||||
{ keys = "Ctrl+w", command = { "quit" } },
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, expected in ipairs(expected_mpv_bindings) do
|
|
||||||
local binding = find_binding(expected.keys)
|
|
||||||
assert_true(binding ~= nil, "default mpv binding should register " .. expected.keys)
|
|
||||||
binding.fn()
|
|
||||||
local command = recorded.mpv_commands[#recorded.mpv_commands]
|
|
||||||
assert_true(command ~= nil, "default mpv binding should invoke mpv command " .. expected.keys)
|
|
||||||
for index, value in ipairs(expected.command) do
|
|
||||||
assert_true(command[index] == value, "default mpv command mismatch for " .. expected.keys)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local expected_cli_bindings = {
|
|
||||||
{ keys = "Shift+]", flag = "--shift-sub-delay-next-line" },
|
|
||||||
{ keys = "Shift+[", flag = "--shift-sub-delay-prev-line" },
|
|
||||||
{ keys = "Ctrl+Alt+c", flag = "--open-youtube-picker" },
|
|
||||||
{ keys = "Ctrl+Alt+p", flag = "--open-playlist-browser" },
|
|
||||||
{ keys = "Ctrl+H", flag = "--replay-current-subtitle" },
|
|
||||||
{ keys = "Ctrl+L", flag = "--play-next-subtitle" },
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, expected in ipairs(expected_cli_bindings) do
|
|
||||||
local binding = find_binding(expected.keys)
|
|
||||||
assert_true(binding ~= nil, "default session action should register " .. expected.keys)
|
|
||||||
binding.fn()
|
|
||||||
local cli_call = recorded.async_calls[#recorded.async_calls]
|
|
||||||
assert_true(cli_call ~= nil, "default session action should invoke CLI " .. expected.keys)
|
|
||||||
assert_true(cli_call[2] == expected.flag, "default session action should pass " .. expected.flag)
|
|
||||||
end
|
|
||||||
|
|
||||||
local play_next = find_binding("Ctrl+L")
|
|
||||||
assert_true(play_next ~= nil, "play-next subtitle binding should use mpv shifted-letter form")
|
assert_true(play_next ~= nil, "play-next subtitle binding should use mpv shifted-letter form")
|
||||||
|
|
||||||
local subtitle_jump = find_binding("L")
|
local subtitle_jump = nil
|
||||||
|
for _, binding in ipairs(recorded.bindings) do
|
||||||
|
if binding.keys == "L" then
|
||||||
|
subtitle_jump = binding
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
assert_true(subtitle_jump ~= nil, "shifted subtitle jump binding should use mpv uppercase letter form")
|
assert_true(subtitle_jump ~= nil, "shifted subtitle jump binding should use mpv uppercase letter form")
|
||||||
|
|
||||||
play_next.fn()
|
play_next.fn()
|
||||||
@@ -329,7 +161,13 @@ local play_next_call = recorded.async_calls[#recorded.async_calls]
|
|||||||
assert_true(play_next_call ~= nil, "play-next binding should invoke CLI action")
|
assert_true(play_next_call ~= nil, "play-next binding should invoke CLI action")
|
||||||
assert_true(play_next_call[2] == "--play-next-subtitle", "play-next binding should pass CLI flag")
|
assert_true(play_next_call[2] == "--play-next-subtitle", "play-next binding should pass CLI flag")
|
||||||
|
|
||||||
local character_dictionary = find_binding("Alt+Meta+a")
|
local character_dictionary = nil
|
||||||
|
for _, binding in ipairs(recorded.bindings) do
|
||||||
|
if binding.keys == "Alt+Meta+a" then
|
||||||
|
character_dictionary = binding
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
assert_true(character_dictionary ~= nil, "character dictionary binding should be registered")
|
assert_true(character_dictionary ~= nil, "character dictionary binding should be registered")
|
||||||
|
|
||||||
character_dictionary.fn()
|
character_dictionary.fn()
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import * as fs from 'fs';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { ConfigService, ConfigStartupParseError } from './service';
|
import { ConfigService, ConfigStartupParseError } from './service';
|
||||||
import {
|
import { DEFAULT_CONFIG, RUNTIME_OPTION_REGISTRY, deepMergeRawConfig } from './definitions';
|
||||||
DEFAULT_CONFIG,
|
|
||||||
DEFAULT_KEYBINDINGS,
|
|
||||||
RUNTIME_OPTION_REGISTRY,
|
|
||||||
deepMergeRawConfig,
|
|
||||||
} from './definitions';
|
|
||||||
import { parseConfigContent } from './parse';
|
|
||||||
import { generateConfigTemplate } from './template';
|
import { generateConfigTemplate } from './template';
|
||||||
|
|
||||||
function makeTempDir(): string {
|
function makeTempDir(): string {
|
||||||
@@ -62,6 +56,7 @@ test('loads defaults when config is missing', () => {
|
|||||||
assert.equal(config.discordPresence.enabled, true);
|
assert.equal(config.discordPresence.enabled, true);
|
||||||
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
assert.equal(config.discordPresence.updateIntervalMs, 3_000);
|
||||||
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
|
||||||
|
assert.equal(config.subtitleStyle.primaryDefaultMode, 'visible');
|
||||||
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
|
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
|
||||||
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
|
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
|
||||||
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, true);
|
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, true);
|
||||||
@@ -2223,12 +2218,3 @@ test('template generator includes known keys', () => {
|
|||||||
/"launchAtStartup": true,? \/\/ Launch texthooker server automatically when SubMiner starts\. Values: true \| false/,
|
/"launchAtStartup": true,? \/\/ Launch texthooker server automatically when SubMiner starts\. Values: true \| false/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('template generator shows built-in default keybindings in the keybindings array', () => {
|
|
||||||
const output = generateConfigTemplate(DEFAULT_CONFIG);
|
|
||||||
const parsed = parseConfigContent('config.example.jsonc', output) as {
|
|
||||||
keybindings?: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.deepEqual(parsed.keybindings, DEFAULT_KEYBINDINGS);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ResolvedConfig } from '../../types/config';
|
|||||||
|
|
||||||
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
|
primaryDefaultMode: 'visible',
|
||||||
enableJlpt: false,
|
enableJlpt: false,
|
||||||
preserveLineBreaks: false,
|
preserveLineBreaks: false,
|
||||||
autoPauseVideoOnHover: true,
|
autoPauseVideoOnHover: true,
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export function buildSubtitleConfigOptionRegistry(
|
|||||||
defaultConfig: ResolvedConfig,
|
defaultConfig: ResolvedConfig,
|
||||||
): ConfigOptionRegistryEntry[] {
|
): ConfigOptionRegistryEntry[] {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
path: 'subtitleStyle.primaryDefaultMode',
|
||||||
|
kind: 'enum',
|
||||||
|
enumValues: ['hidden', 'visible', 'hover'],
|
||||||
|
defaultValue: defaultConfig.subtitleStyle.primaryDefaultMode,
|
||||||
|
description:
|
||||||
|
'Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'subtitleStyle.enableJlpt',
|
path: 'subtitleStyle.enableJlpt',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
{
|
{
|
||||||
title: 'Keybindings (MPV Commands)',
|
title: 'Keybindings (MPV Commands)',
|
||||||
description: [
|
description: [
|
||||||
'Default and custom keybindings that are merged with built-in defaults.',
|
'Extra keybindings that are merged with built-in defaults.',
|
||||||
'Set command to null to disable a default keybinding.',
|
'Set command to null to disable a default keybinding.',
|
||||||
],
|
],
|
||||||
notes: [
|
notes: [
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
|
|
||||||
if (isObject(src.subtitleStyle)) {
|
if (isObject(src.subtitleStyle)) {
|
||||||
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
|
||||||
|
const fallbackSubtitleStylePrimaryDefaultMode = resolved.subtitleStyle.primaryDefaultMode;
|
||||||
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
|
||||||
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
|
||||||
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
|
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
|
||||||
@@ -190,6 +191,24 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const primaryDefaultMode = (src.subtitleStyle as { primaryDefaultMode?: unknown })
|
||||||
|
.primaryDefaultMode;
|
||||||
|
if (
|
||||||
|
primaryDefaultMode === 'hidden' ||
|
||||||
|
primaryDefaultMode === 'visible' ||
|
||||||
|
primaryDefaultMode === 'hover'
|
||||||
|
) {
|
||||||
|
resolved.subtitleStyle.primaryDefaultMode = primaryDefaultMode;
|
||||||
|
} else if (primaryDefaultMode !== undefined) {
|
||||||
|
resolved.subtitleStyle.primaryDefaultMode = fallbackSubtitleStylePrimaryDefaultMode;
|
||||||
|
warn(
|
||||||
|
'subtitleStyle.primaryDefaultMode',
|
||||||
|
primaryDefaultMode,
|
||||||
|
resolved.subtitleStyle.primaryDefaultMode,
|
||||||
|
'Expected hidden, visible, or hover.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const preserveLineBreaks = asBoolean(
|
const preserveLineBreaks = asBoolean(
|
||||||
(src.subtitleStyle as { preserveLineBreaks?: unknown }).preserveLineBreaks,
|
(src.subtitleStyle as { preserveLineBreaks?: unknown }).preserveLineBreaks,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -66,6 +66,31 @@ test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subtitleStyle primaryDefaultMode accepts valid values and warns on invalid', () => {
|
||||||
|
const valid = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
primaryDefaultMode: 'hover',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
applySubtitleDomainConfig(valid.context);
|
||||||
|
assert.equal(valid.context.resolved.subtitleStyle.primaryDefaultMode, 'hover');
|
||||||
|
|
||||||
|
const invalid = createResolveContext({
|
||||||
|
subtitleStyle: {
|
||||||
|
primaryDefaultMode: 'auto' as never,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
applySubtitleDomainConfig(invalid.context);
|
||||||
|
assert.equal(invalid.context.resolved.subtitleStyle.primaryDefaultMode, 'visible');
|
||||||
|
assert.ok(
|
||||||
|
invalid.warnings.some(
|
||||||
|
(warning) =>
|
||||||
|
warning.path === 'subtitleStyle.primaryDefaultMode' &&
|
||||||
|
warning.message === 'Expected hidden, visible, or hover.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('subtitleStyle nameMatchEnabled falls back on invalid value', () => {
|
test('subtitleStyle nameMatchEnabled falls back on invalid value', () => {
|
||||||
const { context, warnings } = createResolveContext({
|
const { context, warnings } = createResolveContext({
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
|
|||||||
+1
-14
@@ -3,7 +3,6 @@ import {
|
|||||||
CONFIG_OPTION_REGISTRY,
|
CONFIG_OPTION_REGISTRY,
|
||||||
CONFIG_TEMPLATE_SECTIONS,
|
CONFIG_TEMPLATE_SECTIONS,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
DEFAULT_KEYBINDINGS,
|
|
||||||
deepCloneConfig,
|
deepCloneConfig,
|
||||||
} from './definitions';
|
} from './definitions';
|
||||||
|
|
||||||
@@ -104,21 +103,9 @@ function renderSection(
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTemplateConfig(config: ResolvedConfig): ResolvedConfig {
|
|
||||||
const templateConfig = deepCloneConfig(config);
|
|
||||||
if (templateConfig.keybindings.length === 0) {
|
|
||||||
templateConfig.keybindings = DEFAULT_KEYBINDINGS.map((binding) => ({
|
|
||||||
key: binding.key,
|
|
||||||
command: binding.command === null ? null : [...binding.command],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return templateConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateConfigTemplate(
|
export function generateConfigTemplate(
|
||||||
config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG),
|
config: ResolvedConfig = deepCloneConfig(DEFAULT_CONFIG),
|
||||||
): string {
|
): string {
|
||||||
const templateConfig = createTemplateConfig(config);
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push('/**');
|
lines.push('/**');
|
||||||
lines.push(' * SubMiner Example Configuration File');
|
lines.push(' * SubMiner Example Configuration File');
|
||||||
@@ -136,7 +123,7 @@ export function generateConfigTemplate(
|
|||||||
lines.push(
|
lines.push(
|
||||||
renderSection(
|
renderSection(
|
||||||
section.key,
|
section.key,
|
||||||
templateConfig[section.key],
|
config[section.key],
|
||||||
index === CONFIG_TEMPLATE_SECTIONS.length - 1,
|
index === CONFIG_TEMPLATE_SECTIONS.length - 1,
|
||||||
comments,
|
comments,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -209,41 +209,6 @@ test('compileSessionBindings keeps default replay and next subtitle session acti
|
|||||||
assert.equal(next?.actionId, 'playNextSubtitle');
|
assert.equal(next?.actionId, 'playNextSubtitle');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('compileSessionBindings wires every default keybinding to an overlay or mpv action', () => {
|
|
||||||
const expectedSpecialActions: Record<string, string> = {
|
|
||||||
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_PREVIOUS_SUBTITLE_START]: 'shiftSubDelayPrevLine',
|
|
||||||
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_NEXT_SUBTITLE_START]: 'shiftSubDelayNextLine',
|
|
||||||
[SPECIAL_COMMANDS.YOUTUBE_PICKER_OPEN]: 'openYoutubePicker',
|
|
||||||
[SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN]: 'openPlaylistBrowser',
|
|
||||||
[SPECIAL_COMMANDS.REPLAY_SUBTITLE]: 'replayCurrentSubtitle',
|
|
||||||
[SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE]: 'playNextSubtitle',
|
|
||||||
};
|
|
||||||
const result = compileSessionBindings({
|
|
||||||
shortcuts: createShortcuts(),
|
|
||||||
keybindings: DEFAULT_KEYBINDINGS,
|
|
||||||
platform: 'linux',
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(result.warnings, []);
|
|
||||||
const byOriginalKey = new Map(result.bindings.map((binding) => [binding.originalKey, binding]));
|
|
||||||
assert.equal(byOriginalKey.size, DEFAULT_KEYBINDINGS.length);
|
|
||||||
|
|
||||||
for (const defaultBinding of DEFAULT_KEYBINDINGS) {
|
|
||||||
const compiled = byOriginalKey.get(defaultBinding.key);
|
|
||||||
assert.ok(compiled, `${defaultBinding.key} should compile`);
|
|
||||||
|
|
||||||
const specialAction = expectedSpecialActions[String(defaultBinding.command?.[0])];
|
|
||||||
if (specialAction) {
|
|
||||||
assert.equal(compiled.actionType, 'session-action');
|
|
||||||
assert.equal(compiled.actionId, specialAction);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.equal(compiled.actionType, 'mpv-command');
|
|
||||||
assert.deepEqual(compiled.command, defaultBinding.command);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('compileSessionBindings omits disabled bindings', () => {
|
test('compileSessionBindings omits disabled bindings', () => {
|
||||||
const result = compileSessionBindings({
|
const result = compileSessionBindings({
|
||||||
shortcuts: createShortcuts({
|
shortcuts: createShortcuts({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { DEFAULT_CONFIG, deepCloneConfig } from '../../config';
|
import { DEFAULT_CONFIG, deepCloneConfig } from '../../config';
|
||||||
import {
|
import {
|
||||||
|
buildConfigHotReloadPayload,
|
||||||
buildRestartRequiredConfigMessage,
|
buildRestartRequiredConfigMessage,
|
||||||
createConfigHotReloadAppliedHandler,
|
createConfigHotReloadAppliedHandler,
|
||||||
createConfigHotReloadMessageHandler,
|
createConfigHotReloadMessageHandler,
|
||||||
@@ -56,6 +57,17 @@ test('createConfigHotReloadAppliedHandler runs all hot-reload effects', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildConfigHotReloadPayload includes independent primary subtitle mode', () => {
|
||||||
|
const config = deepCloneConfig(DEFAULT_CONFIG);
|
||||||
|
config.subtitleStyle.primaryDefaultMode = 'hover';
|
||||||
|
config.secondarySub.defaultMode = 'hidden';
|
||||||
|
|
||||||
|
const payload = buildConfigHotReloadPayload(config);
|
||||||
|
|
||||||
|
assert.equal(payload.primarySubMode, 'hover');
|
||||||
|
assert.equal(payload.secondarySubMode, 'hidden');
|
||||||
|
});
|
||||||
|
|
||||||
test('createConfigHotReloadAppliedHandler skips optional effects when no hot fields', () => {
|
test('createConfigHotReloadAppliedHandler skips optional effects when no hot fields', () => {
|
||||||
const config = deepCloneConfig(DEFAULT_CONFIG);
|
const config = deepCloneConfig(DEFAULT_CONFIG);
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotRe
|
|||||||
sessionBindingWarnings,
|
sessionBindingWarnings,
|
||||||
subtitleStyle: resolveSubtitleStyleForRenderer(config),
|
subtitleStyle: resolveSubtitleStyleForRenderer(config),
|
||||||
subtitleSidebar: config.subtitleSidebar,
|
subtitleSidebar: config.subtitleSidebar,
|
||||||
|
primarySubMode: config.subtitleStyle.primaryDefaultMode,
|
||||||
secondarySubMode: config.secondarySub.defaultMode,
|
secondarySubMode: config.secondarySub.defaultMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import test from 'node:test';
|
|||||||
import { createKeyboardHandlers } from './keyboard.js';
|
import { createKeyboardHandlers } from './keyboard.js';
|
||||||
import { createRendererState } from '../state.js';
|
import { createRendererState } from '../state.js';
|
||||||
import type { CompiledSessionBinding } from '../../types';
|
import type { CompiledSessionBinding } from '../../types';
|
||||||
import { DEFAULT_KEYBINDINGS, SPECIAL_COMMANDS } from '../../config/definitions';
|
|
||||||
import { compileSessionBindings } from '../../core/services/session-bindings';
|
|
||||||
import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config';
|
|
||||||
import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js';
|
import { YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HIDDEN_EVENT } from '../yomitan-popup.js';
|
||||||
|
|
||||||
type CommandEventDetail = {
|
type CommandEventDetail = {
|
||||||
@@ -43,58 +40,6 @@ function wait(ms: number): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventFromKeyString(keyString: string): {
|
|
||||||
key: string;
|
|
||||||
code: string;
|
|
||||||
ctrlKey?: boolean;
|
|
||||||
metaKey?: boolean;
|
|
||||||
altKey?: boolean;
|
|
||||||
shiftKey?: boolean;
|
|
||||||
} {
|
|
||||||
const parts = keyString.split('+');
|
|
||||||
const code = parts.pop() ?? '';
|
|
||||||
return {
|
|
||||||
key: code === 'Space' ? ' ' : code,
|
|
||||||
code,
|
|
||||||
ctrlKey: parts.includes('Ctrl'),
|
|
||||||
metaKey: parts.includes('Meta'),
|
|
||||||
altKey: parts.includes('Alt'),
|
|
||||||
shiftKey: parts.includes('Shift'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function countedJsonValues(values: unknown[]): Array<[string, number]> {
|
|
||||||
const counts = new Map<string, number>();
|
|
||||||
for (const value of values) {
|
|
||||||
const key = JSON.stringify(value);
|
|
||||||
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEmptyShortcuts(): ConfiguredShortcuts {
|
|
||||||
return {
|
|
||||||
toggleVisibleOverlayGlobal: null,
|
|
||||||
copySubtitle: null,
|
|
||||||
copySubtitleMultiple: null,
|
|
||||||
updateLastCardFromClipboard: null,
|
|
||||||
triggerFieldGrouping: null,
|
|
||||||
triggerSubsync: null,
|
|
||||||
mineSentence: null,
|
|
||||||
mineSentenceMultiple: null,
|
|
||||||
multiCopyTimeoutMs: 3000,
|
|
||||||
toggleSecondarySub: null,
|
|
||||||
markAudioCard: null,
|
|
||||||
openCharacterDictionary: null,
|
|
||||||
openRuntimeOptions: null,
|
|
||||||
openJimaku: null,
|
|
||||||
openSessionHelp: null,
|
|
||||||
openControllerSelect: null,
|
|
||||||
openControllerDebug: null,
|
|
||||||
toggleSubtitleSidebar: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function installKeyboardTestGlobals() {
|
function installKeyboardTestGlobals() {
|
||||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||||
const previousDocument = (globalThis as { document?: unknown }).document;
|
const previousDocument = (globalThis as { document?: unknown }).document;
|
||||||
@@ -462,21 +407,37 @@ function createKeyboardHandlerHarness() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('primary subtitle visibility key hides and restores the subtitle bar without mpv sub-visibility', async () => {
|
test('primary subtitle visibility key cycles modes with primary OSD without mpv sub-visibility', async () => {
|
||||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handlers.setupMpvInputForwarding();
|
await handlers.setupMpvInputForwarding();
|
||||||
|
|
||||||
|
testGlobals.dispatchKeydown({ key: 'v', code: 'KeyV' });
|
||||||
|
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), false);
|
||||||
|
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hover'), true);
|
||||||
|
assert.equal(ctx.state.primarySubtitleMode, 'hover');
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({ key: 'v', code: 'KeyV' });
|
testGlobals.dispatchKeydown({ key: 'v', code: 'KeyV' });
|
||||||
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), true);
|
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), true);
|
||||||
|
assert.equal(ctx.state.primarySubtitleMode, 'hidden');
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({ key: 'v', code: 'KeyV' });
|
testGlobals.dispatchKeydown({ key: 'v', code: 'KeyV' });
|
||||||
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), false);
|
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), false);
|
||||||
|
assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hover'), false);
|
||||||
|
assert.equal(ctx.state.primarySubtitleMode, 'visible');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
testGlobals.mpvCommands.some((command) => command.includes('sub-visibility')),
|
testGlobals.mpvCommands.some((command) => command.includes('sub-visibility')),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
testGlobals.mpvCommands.filter((command) => command[0] === 'show-text'),
|
||||||
|
[
|
||||||
|
['show-text', 'Primary subtitle: hover', '1500'],
|
||||||
|
['show-text', 'Primary subtitle: hidden', '1500'],
|
||||||
|
['show-text', 'Primary subtitle: visible', '1500'],
|
||||||
|
],
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
testGlobals.restore();
|
testGlobals.restore();
|
||||||
}
|
}
|
||||||
@@ -764,52 +725,6 @@ test('popup-visible mpv keybindings still fire for bound keys', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default keybindings dispatch through overlay keyboard handling', async () => {
|
|
||||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
|
||||||
const specialActionIds: Record<string, string> = {
|
|
||||||
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_PREVIOUS_SUBTITLE_START]: 'shiftSubDelayPrevLine',
|
|
||||||
[SPECIAL_COMMANDS.SHIFT_SUB_DELAY_TO_NEXT_SUBTITLE_START]: 'shiftSubDelayNextLine',
|
|
||||||
[SPECIAL_COMMANDS.YOUTUBE_PICKER_OPEN]: 'openYoutubePicker',
|
|
||||||
[SPECIAL_COMMANDS.PLAYLIST_BROWSER_OPEN]: 'openPlaylistBrowser',
|
|
||||||
[SPECIAL_COMMANDS.REPLAY_SUBTITLE]: 'replayCurrentSubtitle',
|
|
||||||
[SPECIAL_COMMANDS.PLAY_NEXT_SUBTITLE]: 'playNextSubtitle',
|
|
||||||
};
|
|
||||||
const compiled = compileSessionBindings({
|
|
||||||
shortcuts: createEmptyShortcuts(),
|
|
||||||
keybindings: DEFAULT_KEYBINDINGS,
|
|
||||||
platform: 'linux',
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
assert.deepEqual(compiled.warnings, []);
|
|
||||||
await handlers.setupMpvInputForwarding();
|
|
||||||
handlers.updateSessionBindings(compiled.bindings);
|
|
||||||
|
|
||||||
for (const binding of DEFAULT_KEYBINDINGS) {
|
|
||||||
testGlobals.dispatchKeydown(eventFromKeyString(binding.key));
|
|
||||||
}
|
|
||||||
await wait(0);
|
|
||||||
|
|
||||||
const expectedMpvCommands = DEFAULT_KEYBINDINGS.filter(
|
|
||||||
(binding) => !specialActionIds[String(binding.command?.[0])],
|
|
||||||
).map((binding) => binding.command);
|
|
||||||
const expectedSessionActions = DEFAULT_KEYBINDINGS.map(
|
|
||||||
(binding) => specialActionIds[String(binding.command?.[0])],
|
|
||||||
).filter(Boolean);
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
countedJsonValues(testGlobals.mpvCommands),
|
|
||||||
countedJsonValues(expectedMpvCommands),
|
|
||||||
);
|
|
||||||
assert.deepEqual(
|
|
||||||
testGlobals.sessionActions.map((action) => action.actionId).sort(),
|
|
||||||
expectedSessionActions.sort(),
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
testGlobals.restore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('paused configured subtitle-jump keybinding re-applies pause after backward seek', async () => {
|
test('paused configured subtitle-jump keybinding re-applies pause after backward seek', async () => {
|
||||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CompiledSessionBinding, ShortcutsConfig } from '../../types';
|
import type { CompiledSessionBinding, PrimarySubMode, ShortcutsConfig } from '../../types';
|
||||||
import type { RendererContext } from '../context';
|
import type { RendererContext } from '../context';
|
||||||
import {
|
import {
|
||||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
@@ -370,13 +370,17 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function togglePrimarySubtitleBarVisibility(): void {
|
function togglePrimarySubtitleBarVisibility(): void {
|
||||||
const visible = !ctx.state.primarySubtitleBarVisible;
|
const modes: PrimarySubMode[] = ['hidden', 'visible', 'hover'];
|
||||||
ctx.state.primarySubtitleBarVisible = visible;
|
const currentIndex = modes.indexOf(ctx.state.primarySubtitleMode);
|
||||||
if (visible) {
|
const nextMode = modes[((currentIndex >= 0 ? currentIndex : 1) + 1) % modes.length]!;
|
||||||
ctx.dom.subtitleContainer.classList.remove('primary-sub-hidden');
|
ctx.state.primarySubtitleMode = nextMode;
|
||||||
} else {
|
ctx.dom.subtitleContainer.classList.remove(
|
||||||
ctx.dom.subtitleContainer.classList.add('primary-sub-hidden');
|
'primary-sub-hidden',
|
||||||
}
|
'primary-sub-visible',
|
||||||
|
'primary-sub-hover',
|
||||||
|
);
|
||||||
|
ctx.dom.subtitleContainer.classList.add(`primary-sub-${nextMode}`);
|
||||||
|
window.electronAPI.sendMpvCommand(['show-text', `Primary subtitle: ${nextMode}`, '1500']);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMarkWatched(): Promise<void> {
|
async function handleMarkWatched(): Promise<void> {
|
||||||
|
|||||||
@@ -672,6 +672,7 @@ async function init(): Promise<void> {
|
|||||||
keyboardHandlers.updateSessionBindings(payload.sessionBindings);
|
keyboardHandlers.updateSessionBindings(payload.sessionBindings);
|
||||||
void keyboardHandlers.refreshConfiguredShortcuts();
|
void keyboardHandlers.refreshConfiguredShortcuts();
|
||||||
subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
|
subtitleRenderer.applySubtitleStyle(payload.subtitleStyle);
|
||||||
|
subtitleRenderer.updatePrimarySubMode(payload.primarySubMode);
|
||||||
subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
|
subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode);
|
||||||
ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;
|
ctx.state.subtitleSidebarConfig = payload.subtitleSidebar;
|
||||||
ctx.state.subtitleSidebarToggleKey = payload.subtitleSidebar.toggleKey;
|
ctx.state.subtitleSidebarToggleKey = payload.subtitleSidebar.toggleKey;
|
||||||
@@ -694,6 +695,7 @@ async function init(): Promise<void> {
|
|||||||
|
|
||||||
const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
|
const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
|
||||||
subtitleRenderer.applySubtitleStyle(initialSubtitleStyle);
|
subtitleRenderer.applySubtitleStyle(initialSubtitleStyle);
|
||||||
|
subtitleRenderer.updatePrimarySubMode(initialSubtitleStyle?.primaryDefaultMode ?? 'visible');
|
||||||
await subtitleSidebarModal.refreshSubtitleSidebarSnapshot();
|
await subtitleSidebarModal.refreshSubtitleSidebarSnapshot();
|
||||||
await subtitleSidebarModal.autoOpenSubtitleSidebarOnStartup();
|
await subtitleSidebarModal.autoOpenSubtitleSidebarOnStartup();
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
RuntimeOptionValue,
|
RuntimeOptionValue,
|
||||||
CharacterDictionarySelectionSnapshot,
|
CharacterDictionarySelectionSnapshot,
|
||||||
|
PrimarySubMode,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
SubtitleSidebarConfig,
|
SubtitleSidebarConfig,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
@@ -134,7 +135,7 @@ export type RendererState = {
|
|||||||
keyboardSelectionVisible: boolean;
|
keyboardSelectionVisible: boolean;
|
||||||
keyboardSelectedWordIndex: number | null;
|
keyboardSelectedWordIndex: number | null;
|
||||||
yomitanPopupVisible: boolean;
|
yomitanPopupVisible: boolean;
|
||||||
primarySubtitleBarVisible: boolean;
|
primarySubtitleMode: PrimarySubMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createRendererState(): RendererState {
|
export function createRendererState(): RendererState {
|
||||||
@@ -245,6 +246,6 @@ export function createRendererState(): RendererState {
|
|||||||
keyboardSelectionVisible: false,
|
keyboardSelectionVisible: false,
|
||||||
keyboardSelectedWordIndex: null,
|
keyboardSelectedWordIndex: null,
|
||||||
yomitanPopupVisible: false,
|
yomitanPopupVisible: false,
|
||||||
primarySubtitleBarVisible: true,
|
primarySubtitleMode: 'visible',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -684,6 +684,16 @@ body.subtitle-sidebar-embedded-open #subtitleContainer {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#subtitleContainer.primary-sub-hover {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#subtitleContainer.primary-sub-hover:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#subtitleContainer.primary-sub-hidden {
|
#subtitleContainer.primary-sub-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -1188,6 +1188,16 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
|
|||||||
);
|
);
|
||||||
assert.match(secondaryHoverBaseBlock, /background:\s*transparent;/);
|
assert.match(secondaryHoverBaseBlock, /background:\s*transparent;/);
|
||||||
|
|
||||||
|
const primaryHoverBlock = extractClassBlock(cssText, '#subtitleContainer.primary-sub-hover');
|
||||||
|
assert.match(primaryHoverBlock, /opacity:\s*0;/);
|
||||||
|
assert.match(primaryHoverBlock, /pointer-events:\s*auto;/);
|
||||||
|
|
||||||
|
const primaryHoverVisibleBlock = extractClassBlock(
|
||||||
|
cssText,
|
||||||
|
'#subtitleContainer.primary-sub-hover:hover',
|
||||||
|
);
|
||||||
|
assert.match(primaryHoverVisibleBlock, /opacity:\s*1;/);
|
||||||
|
|
||||||
const secondaryEmbeddedHoverBlock = extractClassBlock(
|
const secondaryEmbeddedHoverBlock = extractClassBlock(
|
||||||
cssText,
|
cssText,
|
||||||
'body.subtitle-sidebar-embedded-open #secondarySubContainer.secondary-sub-hover',
|
'body.subtitle-sidebar-embedded-open #secondarySubContainer.secondary-sub-hover',
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import type { MergedToken, SecondarySubMode, SubtitleData, SubtitleStyleConfig } from '../types';
|
import type {
|
||||||
|
MergedToken,
|
||||||
|
PrimarySubMode,
|
||||||
|
SecondarySubMode,
|
||||||
|
SubtitleData,
|
||||||
|
SubtitleStyleConfig,
|
||||||
|
} from '../types';
|
||||||
import type { RendererContext } from './context';
|
import type { RendererContext } from './context';
|
||||||
|
|
||||||
type FrequencyRenderSettings = {
|
type FrequencyRenderSettings = {
|
||||||
@@ -613,6 +619,16 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
ctx.dom.secondarySubContainer.classList.add(`secondary-sub-${mode}`);
|
ctx.dom.secondarySubContainer.classList.add(`secondary-sub-${mode}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePrimarySubMode(mode: PrimarySubMode): void {
|
||||||
|
ctx.state.primarySubtitleMode = mode;
|
||||||
|
ctx.dom.subtitleContainer.classList.remove(
|
||||||
|
'primary-sub-hidden',
|
||||||
|
'primary-sub-visible',
|
||||||
|
'primary-sub-hover',
|
||||||
|
);
|
||||||
|
ctx.dom.subtitleContainer.classList.add(`primary-sub-${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
function applySubtitleFontSize(fontSize: number): void {
|
function applySubtitleFontSize(fontSize: number): void {
|
||||||
const clampedSize = Math.max(10, fontSize);
|
const clampedSize = Math.max(10, fontSize);
|
||||||
ctx.dom.subtitleRoot.style.fontSize = `${clampedSize}px`;
|
ctx.dom.subtitleRoot.style.fontSize = `${clampedSize}px`;
|
||||||
@@ -791,6 +807,7 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
applySubtitleStyle,
|
applySubtitleStyle,
|
||||||
renderSecondarySub,
|
renderSecondarySub,
|
||||||
renderSubtitle,
|
renderSubtitle,
|
||||||
|
updatePrimarySubMode,
|
||||||
updateSecondarySubMode,
|
updateSecondarySubMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import type {
|
|||||||
YoutubePickerResolveResult,
|
YoutubePickerResolveResult,
|
||||||
} from './integrations';
|
} from './integrations';
|
||||||
import type {
|
import type {
|
||||||
|
PrimarySubMode,
|
||||||
SecondarySubMode,
|
SecondarySubMode,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
@@ -331,6 +332,7 @@ export interface ConfigHotReloadPayload {
|
|||||||
sessionBindingWarnings: SessionBindingWarning[];
|
sessionBindingWarnings: SessionBindingWarning[];
|
||||||
subtitleStyle: SubtitleStyleConfig | null;
|
subtitleStyle: SubtitleStyleConfig | null;
|
||||||
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
subtitleSidebar: Required<SubtitleSidebarConfig>;
|
||||||
|
primarySubMode: PrimarySubMode;
|
||||||
secondarySubMode: SecondarySubMode;
|
secondarySubMode: SecondarySubMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export interface SubtitleStyle {
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SecondarySubMode = 'hidden' | 'visible' | 'hover';
|
export type SubtitleBarMode = 'hidden' | 'visible' | 'hover';
|
||||||
|
export type PrimarySubMode = SubtitleBarMode;
|
||||||
|
export type SecondarySubMode = SubtitleBarMode;
|
||||||
|
|
||||||
export interface SecondarySubConfig {
|
export interface SecondarySubConfig {
|
||||||
secondarySubLanguages?: string[];
|
secondarySubLanguages?: string[];
|
||||||
@@ -67,6 +69,7 @@ export type NPlusOneMatchMode = 'headword' | 'surface';
|
|||||||
export type FrequencyDictionaryMatchMode = 'headword' | 'surface';
|
export type FrequencyDictionaryMatchMode = 'headword' | 'surface';
|
||||||
|
|
||||||
export interface SubtitleStyleConfig {
|
export interface SubtitleStyleConfig {
|
||||||
|
primaryDefaultMode?: PrimarySubMode;
|
||||||
enableJlpt?: boolean;
|
enableJlpt?: boolean;
|
||||||
preserveLineBreaks?: boolean;
|
preserveLineBreaks?: boolean;
|
||||||
autoPauseVideoOnHover?: boolean;
|
autoPauseVideoOnHover?: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user