Compare commits

..

2 Commits

Author SHA1 Message Date
sudacode 6af9a19ff3 feat(overlay): add primary subtitle bar visibility modes
- 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
2026-05-12 20:30:42 -07:00
sudacode 430373f010 feat(tokenizer): use Yomitan word classes for subtitle POS filtering (#57)
* feat(tokenizer): use Yomitan word classes for subtitle POS filtering

- Carry matched headword wordClasses from termsFind into YomitanScanToken
- Map recognized Yomitan wordClasses to SubMiner coarse POS before annotation
- MeCab enrichment now fills only missing POS fields, preserving existing coarse pos1
- Exclude standalone grammar particles, して helper fragments, and single-kana surfaces from annotations
- Respect source-text punctuation gaps when counting N+1 sentence words
- Preserve known-word highlight on excluded kanji-containing tokens
- Add backlog tasks 304 (N+1 boundary bug) and 305 (wordClasses POS, done)

* fix(tokenizer): preserve annotation and enrichment behavior

* fix: restore jlpt subtitle underlines

* fix: exclude kana-only n+1 targets

* fix: refresh overlay on Hyprland fullscreen

* fix: address fullscreen and n-plus-one review notes

* fix: address CodeRabbit review comments

* fix: accept modified digits for multi-line sentence mining

* Cancel pending Linux MPV fullscreen overlay refresh bursts

- return a cancel handle from the Linux refresh burst scheduler
- clear pending refresh bursts when overlays hide or windows close
- tighten the burst test polling to wait for the async refresh

* fix: suppress N+1 for kana-only candidates and fix minSentenceWords coun

- Treat kana-only tokens with surrounding subtitle punctuation (…, ―, etc.) as kana-only so they are not promoted to N+1 targets
- Exclude unknown tokens filtered from N+1 targeting from the minSentenceWords count so filtered kana-only unknowns cannot satisfy sentence length threshold
- Add regression tests for kana-only candidate suppression and filtered-unknown padding cases

* Suppress subtitle annotations for grammar fragments

- Hide annotation metadata for auxiliary inflection and ja-nai endings
- Preserve lexical `くれる` forms and add regression coverage

* Fix kana-only N+1 tokenizer regression test

- Use a pure-kana fixture for the subtitle token N+1 case
- Update task notes for the latest CodeRabbit follow-up

* Fix managed playback exit and tokenizer grammar splits

- Ignore background stats daemons during regular app startup
- Split standalone grammar endings before applying annotations
- Clear helper-span annotations for auxiliary-only tokens

* fix: refresh current subtitle after known-word mining

* fix: suppress sigh interjection annotations

* fix: preserve jlpt underline color after lookup

* Replace grammar-ending permutations with shared matcher; preserve word a

- Extract `grammar-ending.ts` with `isStandaloneGrammarEndingText` / `isSubtitleGrammarEndingText` pattern matchers
- Replace `STANDALONE_GRAMMAR_ENDINGS` set in parser-selection-stage with shared matcher
- Replace generated phrase sets in subtitle-annotation-filter with shared matcher
- Remove stale duplicate subtitle-exclusion constants and helpers from annotation-stage
- Manual clipboard card updates now write only to the sentence audio field, leaving word/expression audio untouched

* fix: CI changelog, annotation options threading, and Jellyfin quit

- Add `type: fixed` / `area:` frontmatter to `changes/319` to pass `changelog:lint`
- Thread `TokenizerAnnotationOptions` through `stripSubtitleAnnotationMetadata` so `sourceText` is honored
- Include `jellyfinPlay` in `shouldQuitOnDisconnectWhenOverlayRuntimeInitialized` predicate
- Make mouse test `elementFromPoint` stubs coordinate-sensitive
- Make Lua test `.tmp` mkdir portable on Windows

* Preserve overlay across macOS flaps and mpv playlist changes

- keep visible overlays alive during transient macOS tracker loss
- reuse the running mpv overlay path on playlist navigation
- update regression coverage and changelog fragments

* fix: restore stats daemon deferral

* fix: keep subtitle prefetch alive after cache hits

* Fix JLPT underline color drift and AniList skipped-threshold sync

- Replace JLPT `text-decoration` underlines with `border-bottom` so Chromium selection/hover cannot repaint them to another annotation's color
- Lock JLPT underline color for combined annotation selectors (known, n+1, frequency) and character hover/selection states
- Trigger AniList post-watch check on every mpv time-position update to catch skipped completion thresholds
- Fall back to filename-parser season/episode when guessit omits them

* fix: address coderabbit feedback

* fix: sync AniList after seeked completion

* fix: preserve ordinal frequency annotations

* fix: preserve known highlighting for filtered tokens

* fix: address PR #57 CodeRabbit feedback

- Acquire AniList post-watch in-flight lock before async gating to prevent duplicate writes
- Isolate manual watched mark result from AniList post-watch callback failures
- Report known-word cache clears as mutations during immediate append when state existed
- Add regression tests for each fix

* fix: stop AniList setup reopening on Linux when keyring token exists

- Gate setup success on token persistence: `saveToken` now returns `boolean`; on failure, keeps the setup window open instead of reporting success
- Config reload passes `allowSetupPrompt: false` so playback reloads don't re-open the setup window
- Add regression test for persistence-failure path

* fix: suppress known highlights for subtitle particles

* fix: retry transient AniList safeStorage failures

* fix: hide overlay focus ring

* fix: align Hyprland fullscreen overlays

* fix: restore subtitle playback keybindings

* fix: align Hyprland overlay windows to mpv and stop pinning them

- Force-apply exact Hyprland move/resize/setprop dispatches when bounds are provided
- Stop pinning overlay windows; toggle pin off when Hyprland reports pinned=true
- Compensate stats overlay outer placement for Electron/Wayland content insets
- Make stats overlay window and page opaque so mpv cannot show through transparent insets
- Constrain stats app to h-screen with internal scroll so content covers mpv from y=0
- Lock overlay/stats window titles against page-title-updated events
- Add regression coverage for placement dispatches, inset compensation, and CSS overlay mode

* fix: retain frequency rank for honorific prefix-noun tokens

- Add `shouldAllowHonorificPrefixNounFrequency` to exempt お/ご/御 + noun merged tokens from frequency exclusion
- Add regression test for `ご機嫌` asserting rank 5484 is preserved after MeCab enrichment and annotation
- Close TASK-341

* fix: map openCharacterDictionary session action to --open-character-dict

- Add missing Lua CLI dispatch entry for openCharacterDictionary
- Add regression test for Alt+Meta+A binding and CLI flag forwarding

* fix: keep macOS overlay interactive while mpv remains active

- Overlay no longer hides or becomes click-through during tracker refreshes when mpv is the focused window
- Preserve already-visible overlay when tracker is temporarily not ready but mpv target signal is active
- Add regression tests for active-mpv tracker refresh and transient tracker-not-ready paths

* fix: address coderabbit subtitle follow-ups

* fix: resolve media detail from sessions when lifetime summary is absent

- Change `getMediaDetail` JOIN to LEFT JOIN on `imm_lifetime_media` and fall back to aggregated session metrics when no lifetime row exists
- Add filter `AND (lm.video_id IS NOT NULL OR s.session_id IS NOT NULL)` to keep results valid
- Add regression test covering the session-visible / media-detail-missing mismatch

* fix: address PR-57 CodeRabbit findings and CI failures

- use filtered word counts in media detail session token aggregation
- cancel fullscreen refresh burst on exit via updateLinuxMpvFullscreenOverlayRefreshBurst
- guard Hyprland JSON.parse in try/catch; exclude windowtitle from geometry events
- narrow focus suppression from :focus to :focus-visible
- apply JLPT lock selectors to word-name-match tokens (N1–N5)

* fix: macOS overlay z-order and Yomitan compound token known highlighting

- Release always-on-top when tracked mpv loses foreground on macOS
- Skip visible overlay blur restacking on macOS to avoid covering unrelated windows
- Prefer Yomitan internal parse tokens over fragmented scanner output for known-word decisions
- Add regression tests for both behaviors

* fix: macOS visible-overlay blur no longer invokes Windows-only blur call

- Split win32/darwin branches in handleOverlayWindowBlurred so darwin visible blur returns early without calling onWindowsVisibleOverlayBlur
- Add regression test asserting Windows callback stays inactive on macOS visible overlay blur
- Close TASK-347
2026-05-12 12:08:09 -07:00
20 changed files with 221 additions and 13 deletions
@@ -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 -->
@@ -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
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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);
@@ -2,6 +2,7 @@ import { ResolvedConfig } from '../../types/config';
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
subtitleStyle: {
primaryDefaultMode: 'visible',
enableJlpt: false,
preserveLineBreaks: false,
autoPauseVideoOnHover: true,
@@ -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',
+19
View File
@@ -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,
);
+25
View File
@@ -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: {
@@ -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[] = [];
@@ -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,
};
}
+17 -1
View File
@@ -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();
}
+12 -8
View File
@@ -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<void> {
+2
View File
@@ -672,6 +672,7 @@ async function init(): Promise<void> {
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<void> {
const initialSubtitleStyle = await window.electronAPI.getSubtitleStyle();
subtitleRenderer.applySubtitleStyle(initialSubtitleStyle);
subtitleRenderer.updatePrimarySubMode(initialSubtitleStyle?.primaryDefaultMode ?? 'visible');
await subtitleSidebarModal.refreshSubtitleSidebarSnapshot();
await subtitleSidebarModal.autoOpenSubtitleSidebarOnStartup();
+3 -2
View File
@@ -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',
};
}
+10
View File
@@ -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;
+10
View File
@@ -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',
+18 -1
View File
@@ -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,
};
}
+2
View File
@@ -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<SubtitleSidebarConfig>;
primarySubMode: PrimarySubMode;
secondarySubMode: SecondarySubMode;
}
+4 -1
View File
@@ -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;