From e5c1135501457ad65b12ab64180f14f15b248654 Mon Sep 17 00:00:00 2001 From: sudacode Date: Tue, 12 May 2026 23:00:32 -0700 Subject: [PATCH] feat(overlay): add primary subtitle bar visibility modes (#63) - Cycle `v` through `hidden | visible | hover` instead of a boolean toggle - Add `subtitleStyle.primaryDefaultMode` config with default `visible` - Carry primary mode independently from secondary in hot-reload payload - Add hover CSS: transparent until hovered, then fully visible - Show primary-specific OSD text on each mode change --- ...d-primary-subtitle-bar-visibility-modes.md | 70 +++++++++++++++++++ .../357-primary-subtitle-visibility-modes.md | 4 ++ config.example.jsonc | 1 + docs-site/public/config.example.jsonc | 1 + src/config/config.test.ts | 1 + src/config/definitions/defaults-subtitle.ts | 1 + src/config/definitions/options-subtitle.ts | 8 +++ src/config/resolve/subtitle-domains.ts | 19 +++++ src/config/resolve/subtitle-style.test.ts | 25 +++++++ .../config-hot-reload-handlers.test.ts | 12 ++++ .../runtime/config-hot-reload-handlers.ts | 1 + src/renderer/handlers/keyboard.test.ts | 18 ++++- src/renderer/handlers/keyboard.ts | 20 +++--- src/renderer/renderer.ts | 2 + src/renderer/state.ts | 5 +- src/renderer/style.css | 10 +++ src/renderer/subtitle-render.test.ts | 10 +++ src/renderer/subtitle-render.ts | 19 ++++- src/types/runtime.ts | 2 + src/types/subtitle.ts | 5 +- 20 files changed, 221 insertions(+), 13 deletions(-) create mode 100644 backlog/tasks/task-357 - Add-primary-subtitle-bar-visibility-modes.md create mode 100644 changes/357-primary-subtitle-visibility-modes.md diff --git a/backlog/tasks/task-357 - Add-primary-subtitle-bar-visibility-modes.md b/backlog/tasks/task-357 - Add-primary-subtitle-bar-visibility-modes.md new file mode 100644 index 00000000..b001e1f8 --- /dev/null +++ b/backlog/tasks/task-357 - Add-primary-subtitle-bar-visibility-modes.md @@ -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 + + +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. + + +## Acceptance Criteria + +- [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. + + +## Implementation Plan + + +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: `. +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. + + +## Implementation Notes + + +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`. + + +## Final Summary + + +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` + diff --git a/changes/357-primary-subtitle-visibility-modes.md b/changes/357-primary-subtitle-visibility-modes.md new file mode 100644 index 00000000..92dbda90 --- /dev/null +++ b/changes/357-primary-subtitle-visibility-modes.md @@ -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. diff --git a/config.example.jsonc b/config.example.jsonc index d734c08a..b4495b9c 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -227,6 +227,7 @@ // Hot-reload: subtitle style changes apply live without restarting SubMiner. // ========================================== "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 "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 diff --git a/docs-site/public/config.example.jsonc b/docs-site/public/config.example.jsonc index d734c08a..b4495b9c 100644 --- a/docs-site/public/config.example.jsonc +++ b/docs-site/public/config.example.jsonc @@ -227,6 +227,7 @@ // Hot-reload: subtitle style changes apply live without restarting SubMiner. // ========================================== "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 "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 diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 1cd057a4..31b95c64 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -56,6 +56,7 @@ test('loads defaults when config is missing', () => { assert.equal(config.discordPresence.enabled, true); assert.equal(config.discordPresence.updateIntervalMs, 3_000); 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.autoPauseVideoOnHover, true); assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, true); diff --git a/src/config/definitions/defaults-subtitle.ts b/src/config/definitions/defaults-subtitle.ts index 88371974..8c8aeabb 100644 --- a/src/config/definitions/defaults-subtitle.ts +++ b/src/config/definitions/defaults-subtitle.ts @@ -2,6 +2,7 @@ import { ResolvedConfig } from '../../types/config'; export const SUBTITLE_DEFAULT_CONFIG: Pick = { subtitleStyle: { + primaryDefaultMode: 'visible', enableJlpt: false, preserveLineBreaks: false, autoPauseVideoOnHover: true, diff --git a/src/config/definitions/options-subtitle.ts b/src/config/definitions/options-subtitle.ts index 5445eb29..f10d54a0 100644 --- a/src/config/definitions/options-subtitle.ts +++ b/src/config/definitions/options-subtitle.ts @@ -5,6 +5,14 @@ export function buildSubtitleConfigOptionRegistry( defaultConfig: ResolvedConfig, ): ConfigOptionRegistryEntry[] { 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', kind: 'boolean', diff --git a/src/config/resolve/subtitle-domains.ts b/src/config/resolve/subtitle-domains.ts index 26612743..c1ed26e6 100644 --- a/src/config/resolve/subtitle-domains.ts +++ b/src/config/resolve/subtitle-domains.ts @@ -147,6 +147,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void { if (isObject(src.subtitleStyle)) { const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt; + const fallbackSubtitleStylePrimaryDefaultMode = resolved.subtitleStyle.primaryDefaultMode; const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks; const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover; 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( (src.subtitleStyle as { preserveLineBreaks?: unknown }).preserveLineBreaks, ); diff --git a/src/config/resolve/subtitle-style.test.ts b/src/config/resolve/subtitle-style.test.ts index 2090d741..311021a2 100644 --- a/src/config/resolve/subtitle-style.test.ts +++ b/src/config/resolve/subtitle-style.test.ts @@ -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', () => { const { context, warnings } = createResolveContext({ subtitleStyle: { diff --git a/src/main/runtime/config-hot-reload-handlers.test.ts b/src/main/runtime/config-hot-reload-handlers.test.ts index 9ad4e1c6..533ab8d2 100644 --- a/src/main/runtime/config-hot-reload-handlers.test.ts +++ b/src/main/runtime/config-hot-reload-handlers.test.ts @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { DEFAULT_CONFIG, deepCloneConfig } from '../../config'; import { + buildConfigHotReloadPayload, buildRestartRequiredConfigMessage, createConfigHotReloadAppliedHandler, 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', () => { const config = deepCloneConfig(DEFAULT_CONFIG); const calls: string[] = []; diff --git a/src/main/runtime/config-hot-reload-handlers.ts b/src/main/runtime/config-hot-reload-handlers.ts index 89f53977..aef0d3bc 100644 --- a/src/main/runtime/config-hot-reload-handlers.ts +++ b/src/main/runtime/config-hot-reload-handlers.ts @@ -54,6 +54,7 @@ export function buildConfigHotReloadPayload(config: ResolvedConfig): ConfigHotRe sessionBindingWarnings, subtitleStyle: resolveSubtitleStyleForRenderer(config), subtitleSidebar: config.subtitleSidebar, + primarySubMode: config.subtitleStyle.primaryDefaultMode, secondarySubMode: config.secondarySub.defaultMode, }; } diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index 7178bac8..0ec1e197 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -407,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(); try { 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' }); assert.equal(ctx.dom.subtitleContainer.classList.contains('primary-sub-hidden'), true); + assert.equal(ctx.state.primarySubtitleMode, 'hidden'); 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'), false); + assert.equal(ctx.state.primarySubtitleMode, 'visible'); assert.equal( testGlobals.mpvCommands.some((command) => command.includes('sub-visibility')), 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 { testGlobals.restore(); } diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 3153ef0a..7d26b774 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -1,4 +1,4 @@ -import type { CompiledSessionBinding, ShortcutsConfig } from '../../types'; +import type { CompiledSessionBinding, PrimarySubMode, ShortcutsConfig } from '../../types'; import type { RendererContext } from '../context'; import { YOMITAN_POPUP_HIDDEN_EVENT, @@ -370,13 +370,17 @@ export function createKeyboardHandlers( } function togglePrimarySubtitleBarVisibility(): void { - const visible = !ctx.state.primarySubtitleBarVisible; - ctx.state.primarySubtitleBarVisible = visible; - if (visible) { - ctx.dom.subtitleContainer.classList.remove('primary-sub-hidden'); - } else { - ctx.dom.subtitleContainer.classList.add('primary-sub-hidden'); - } + const modes: PrimarySubMode[] = ['hidden', 'visible', 'hover']; + const currentIndex = modes.indexOf(ctx.state.primarySubtitleMode); + const nextMode = modes[((currentIndex >= 0 ? currentIndex : 1) + 1) % modes.length]!; + ctx.state.primarySubtitleMode = nextMode; + ctx.dom.subtitleContainer.classList.remove( + '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 { diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 51bcc146..6bd4a292 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -672,6 +672,7 @@ async function init(): Promise { keyboardHandlers.updateSessionBindings(payload.sessionBindings); void keyboardHandlers.refreshConfiguredShortcuts(); subtitleRenderer.applySubtitleStyle(payload.subtitleStyle); + subtitleRenderer.updatePrimarySubMode(payload.primarySubMode); subtitleRenderer.updateSecondarySubMode(payload.secondarySubMode); ctx.state.subtitleSidebarConfig = payload.subtitleSidebar; ctx.state.subtitleSidebarToggleKey = payload.subtitleSidebar.toggleKey; @@ -694,6 +695,7 @@ async function init(): Promise { const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle(); subtitleRenderer.applySubtitleStyle(initialSubtitleStyle); + subtitleRenderer.updatePrimarySubMode(initialSubtitleStyle?.primaryDefaultMode ?? 'visible'); await subtitleSidebarModal.refreshSubtitleSidebarSnapshot(); await subtitleSidebarModal.autoOpenSubtitleSidebarOnStartup(); diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 47806d2c..1b74000a 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -12,6 +12,7 @@ import type { RuntimeOptionState, RuntimeOptionValue, CharacterDictionarySelectionSnapshot, + PrimarySubMode, SubtitlePosition, SubtitleSidebarConfig, SubtitleCue, @@ -134,7 +135,7 @@ export type RendererState = { keyboardSelectionVisible: boolean; keyboardSelectedWordIndex: number | null; yomitanPopupVisible: boolean; - primarySubtitleBarVisible: boolean; + primarySubtitleMode: PrimarySubMode; }; export function createRendererState(): RendererState { @@ -245,6 +246,6 @@ export function createRendererState(): RendererState { keyboardSelectionVisible: false, keyboardSelectedWordIndex: null, yomitanPopupVisible: false, - primarySubtitleBarVisible: true, + primarySubtitleMode: 'visible', }; } diff --git a/src/renderer/style.css b/src/renderer/style.css index cf8d3875..9c3e0310 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -684,6 +684,16 @@ body.subtitle-sidebar-embedded-open #subtitleContainer { 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 { display: none; pointer-events: none; diff --git a/src/renderer/subtitle-render.test.ts b/src/renderer/subtitle-render.test.ts index c008bc8c..932c3b11 100644 --- a/src/renderer/subtitle-render.test.ts +++ b/src/renderer/subtitle-render.test.ts @@ -1188,6 +1188,16 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo ); 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( cssText, 'body.subtitle-sidebar-embedded-open #secondarySubContainer.secondary-sub-hover', diff --git a/src/renderer/subtitle-render.ts b/src/renderer/subtitle-render.ts index 94d95710..3836dd52 100644 --- a/src/renderer/subtitle-render.ts +++ b/src/renderer/subtitle-render.ts @@ -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'; type FrequencyRenderSettings = { @@ -613,6 +619,16 @@ export function createSubtitleRenderer(ctx: RendererContext) { 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 { const clampedSize = Math.max(10, fontSize); ctx.dom.subtitleRoot.style.fontSize = `${clampedSize}px`; @@ -791,6 +807,7 @@ export function createSubtitleRenderer(ctx: RendererContext) { applySubtitleStyle, renderSecondarySub, renderSubtitle, + updatePrimarySubMode, updateSecondarySubMode, }; } diff --git a/src/types/runtime.ts b/src/types/runtime.ts index 022a77c9..c2f26c53 100644 --- a/src/types/runtime.ts +++ b/src/types/runtime.ts @@ -25,6 +25,7 @@ import type { YoutubePickerResolveResult, } from './integrations'; import type { + PrimarySubMode, SecondarySubMode, SubtitleData, SubtitlePosition, @@ -331,6 +332,7 @@ export interface ConfigHotReloadPayload { sessionBindingWarnings: SessionBindingWarning[]; subtitleStyle: SubtitleStyleConfig | null; subtitleSidebar: Required; + primarySubMode: PrimarySubMode; secondarySubMode: SecondarySubMode; } diff --git a/src/types/subtitle.ts b/src/types/subtitle.ts index d744b200..0da3dec2 100644 --- a/src/types/subtitle.ts +++ b/src/types/subtitle.ts @@ -55,7 +55,9 @@ export interface SubtitleStyle { 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 { secondarySubLanguages?: string[]; @@ -67,6 +69,7 @@ export type NPlusOneMatchMode = 'headword' | 'surface'; export type FrequencyDictionaryMatchMode = 'headword' | 'surface'; export interface SubtitleStyleConfig { + primaryDefaultMode?: PrimarySubMode; enableJlpt?: boolean; preserveLineBreaks?: boolean; autoPauseVideoOnHover?: boolean;