mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-13 08:12:54 -07:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e8f10fe8a9
|
|||
|
ca796bfe6a
|
|||
|
6bf905140c
|
|||
|
6e666d7ca5
|
|||
|
27be0e6fd7
|
|||
|
eff33e2027
|
|||
|
47499eccff
|
|||
|
0b72fa108f
|
|||
|
2b60c20711
|
|||
|
8f43f8825d
|
|||
|
5396b08972
|
|||
|
934a7281b0
|
|||
|
4497d0a39f
|
|||
|
2d1e51e7e1
|
|||
|
c97888f811
|
|||
|
77f5a48f5d
|
|||
|
f2fb9fa1b9
|
|||
|
3284c40ab5
|
|||
|
4bd8fc3db4
|
|||
|
837f21b346
|
|||
|
12e1e783c9
|
|||
|
42576d99b1
|
|||
|
a2fd3cd194
|
|||
|
805b68dd92
|
|||
|
dacae39544
|
|||
|
41b2c7eccf
|
|||
|
9c8784672c
|
|||
|
cb1650d366
|
|||
|
f17255c8e2
|
|||
|
1c1f498f9e
|
|||
|
939a0e650e
|
|||
|
166cdb06ec
|
|||
|
a69e5ecfdf
|
|||
|
d991499dda
|
|||
|
6053a1b6ac
|
|||
|
5cc5df4b18
|
|||
|
d92a2072eb
|
|||
|
2bb7be3552
|
|||
|
0855a7dfcc
|
|||
|
077c852a08
|
|||
|
09e10c18d2
|
|||
|
8b26559203
|
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
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,4 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -227,7 +227,6 @@
|
|||||||
// 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
|
||||||
|
|||||||
@@ -227,7 +227,6 @@
|
|||||||
// 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
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ 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);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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,14 +5,6 @@ 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',
|
||||||
|
|||||||
@@ -147,7 +147,6 @@ 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 =
|
||||||
@@ -191,24 +190,6 @@ 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,31 +66,6 @@ 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: {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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,
|
||||||
@@ -57,17 +56,6 @@ 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,7 +54,6 @@ 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,37 +407,21 @@ function createKeyboardHandlerHarness() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('primary subtitle visibility key cycles modes with primary OSD without mpv sub-visibility', async () => {
|
test('primary subtitle visibility key hides and restores the subtitle bar 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CompiledSessionBinding, PrimarySubMode, ShortcutsConfig } from '../../types';
|
import type { CompiledSessionBinding, ShortcutsConfig } from '../../types';
|
||||||
import type { RendererContext } from '../context';
|
import type { RendererContext } from '../context';
|
||||||
import {
|
import {
|
||||||
YOMITAN_POPUP_HIDDEN_EVENT,
|
YOMITAN_POPUP_HIDDEN_EVENT,
|
||||||
@@ -370,17 +370,13 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function togglePrimarySubtitleBarVisibility(): void {
|
function togglePrimarySubtitleBarVisibility(): void {
|
||||||
const modes: PrimarySubMode[] = ['hidden', 'visible', 'hover'];
|
const visible = !ctx.state.primarySubtitleBarVisible;
|
||||||
const currentIndex = modes.indexOf(ctx.state.primarySubtitleMode);
|
ctx.state.primarySubtitleBarVisible = visible;
|
||||||
const nextMode = modes[((currentIndex >= 0 ? currentIndex : 1) + 1) % modes.length]!;
|
if (visible) {
|
||||||
ctx.state.primarySubtitleMode = nextMode;
|
ctx.dom.subtitleContainer.classList.remove('primary-sub-hidden');
|
||||||
ctx.dom.subtitleContainer.classList.remove(
|
} else {
|
||||||
'primary-sub-hidden',
|
ctx.dom.subtitleContainer.classList.add('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,7 +672,6 @@ 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;
|
||||||
@@ -695,7 +694,6 @@ 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,7 +12,6 @@ import type {
|
|||||||
RuntimeOptionState,
|
RuntimeOptionState,
|
||||||
RuntimeOptionValue,
|
RuntimeOptionValue,
|
||||||
CharacterDictionarySelectionSnapshot,
|
CharacterDictionarySelectionSnapshot,
|
||||||
PrimarySubMode,
|
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
SubtitleSidebarConfig,
|
SubtitleSidebarConfig,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
@@ -135,7 +134,7 @@ export type RendererState = {
|
|||||||
keyboardSelectionVisible: boolean;
|
keyboardSelectionVisible: boolean;
|
||||||
keyboardSelectedWordIndex: number | null;
|
keyboardSelectedWordIndex: number | null;
|
||||||
yomitanPopupVisible: boolean;
|
yomitanPopupVisible: boolean;
|
||||||
primarySubtitleMode: PrimarySubMode;
|
primarySubtitleBarVisible: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createRendererState(): RendererState {
|
export function createRendererState(): RendererState {
|
||||||
@@ -246,6 +245,6 @@ export function createRendererState(): RendererState {
|
|||||||
keyboardSelectionVisible: false,
|
keyboardSelectionVisible: false,
|
||||||
keyboardSelectedWordIndex: null,
|
keyboardSelectedWordIndex: null,
|
||||||
yomitanPopupVisible: false,
|
yomitanPopupVisible: false,
|
||||||
primarySubtitleMode: 'visible',
|
primarySubtitleBarVisible: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -684,16 +684,6 @@ 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,16 +1188,6 @@ 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,10 +1,4 @@
|
|||||||
import type {
|
import type { MergedToken, SecondarySubMode, SubtitleData, SubtitleStyleConfig } from '../types';
|
||||||
MergedToken,
|
|
||||||
PrimarySubMode,
|
|
||||||
SecondarySubMode,
|
|
||||||
SubtitleData,
|
|
||||||
SubtitleStyleConfig,
|
|
||||||
} from '../types';
|
|
||||||
import type { RendererContext } from './context';
|
import type { RendererContext } from './context';
|
||||||
|
|
||||||
type FrequencyRenderSettings = {
|
type FrequencyRenderSettings = {
|
||||||
@@ -619,16 +613,6 @@ 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`;
|
||||||
@@ -807,7 +791,6 @@ export function createSubtitleRenderer(ctx: RendererContext) {
|
|||||||
applySubtitleStyle,
|
applySubtitleStyle,
|
||||||
renderSecondarySub,
|
renderSecondarySub,
|
||||||
renderSubtitle,
|
renderSubtitle,
|
||||||
updatePrimarySubMode,
|
|
||||||
updateSecondarySubMode,
|
updateSecondarySubMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import type {
|
|||||||
YoutubePickerResolveResult,
|
YoutubePickerResolveResult,
|
||||||
} from './integrations';
|
} from './integrations';
|
||||||
import type {
|
import type {
|
||||||
PrimarySubMode,
|
|
||||||
SecondarySubMode,
|
SecondarySubMode,
|
||||||
SubtitleData,
|
SubtitleData,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
@@ -332,7 +331,6 @@ 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,9 +55,7 @@ export interface SubtitleStyle {
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SubtitleBarMode = 'hidden' | 'visible' | 'hover';
|
export type SecondarySubMode = 'hidden' | 'visible' | 'hover';
|
||||||
export type PrimarySubMode = SubtitleBarMode;
|
|
||||||
export type SecondarySubMode = SubtitleBarMode;
|
|
||||||
|
|
||||||
export interface SecondarySubConfig {
|
export interface SecondarySubConfig {
|
||||||
secondarySubLanguages?: string[];
|
secondarySubLanguages?: string[];
|
||||||
@@ -69,7 +67,6 @@ 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