feat(renderer): add optional yomitan popup auto-pause

This commit is contained in:
2026-03-04 11:19:46 -08:00
parent 9a30419a23
commit 69ab87c25f
18 changed files with 474 additions and 9 deletions

View File

@@ -33,6 +33,7 @@ test('loads defaults when config is missing', () => {
assert.equal(config.subtitleStyle.backgroundColor, 'rgb(30, 32, 48, 0.88)');
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6');
assert.equal(config.subtitleStyle.hoverTokenBackgroundColor, 'rgba(54, 58, 79, 0.84)');
assert.equal(
@@ -160,6 +161,44 @@ test('parses subtitleStyle.autoPauseVideoOnHover and warns on invalid values', (
);
});
test('parses subtitleStyle.autoPauseVideoOnYomitanPopup and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(
path.join(validDir, 'config.jsonc'),
`{
"subtitleStyle": {
"autoPauseVideoOnYomitanPopup": true
}
}`,
'utf-8',
);
const validService = new ConfigService(validDir);
assert.equal(validService.getConfig().subtitleStyle.autoPauseVideoOnYomitanPopup, true);
const invalidDir = makeTempDir();
fs.writeFileSync(
path.join(invalidDir, 'config.jsonc'),
`{
"subtitleStyle": {
"autoPauseVideoOnYomitanPopup": "yes"
}
}`,
'utf-8',
);
const invalidService = new ConfigService(invalidDir);
assert.equal(
invalidService.getConfig().subtitleStyle.autoPauseVideoOnYomitanPopup,
DEFAULT_CONFIG.subtitleStyle.autoPauseVideoOnYomitanPopup,
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'subtitleStyle.autoPauseVideoOnYomitanPopup'),
);
});
test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(

View File

@@ -5,6 +5,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
enableJlpt: false,
preserveLineBreaks: false,
autoPauseVideoOnHover: true,
autoPauseVideoOnYomitanPopup: false,
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',

View File

@@ -20,6 +20,7 @@ test('config option registry includes critical paths and has unique entries', ()
'logging.level',
'startupWarmups.lowPowerMode',
'subtitleStyle.enableJlpt',
'subtitleStyle.autoPauseVideoOnYomitanPopup',
'ankiConnect.enabled',
'immersionTracking.enabled',
]) {

View File

@@ -28,6 +28,13 @@ export function buildSubtitleConfigOptionRegistry(
description:
'Automatically pause mpv playback while hovering subtitle text, then resume on leave.',
},
{
path: 'subtitleStyle.autoPauseVideoOnYomitanPopup',
kind: 'boolean',
defaultValue: defaultConfig.subtitleStyle.autoPauseVideoOnYomitanPopup,
description:
'Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes.',
},
{
path: 'subtitleStyle.hoverTokenColor',
kind: 'string',

View File

@@ -100,6 +100,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup;
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
const fallbackSubtitleStyleHoverTokenBackgroundColor =
resolved.subtitleStyle.hoverTokenBackgroundColor;
@@ -171,6 +173,26 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const autoPauseVideoOnYomitanPopup = asBoolean(
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown }).autoPauseVideoOnYomitanPopup,
);
if (autoPauseVideoOnYomitanPopup !== undefined) {
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup = autoPauseVideoOnYomitanPopup;
} else if (
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown })
.autoPauseVideoOnYomitanPopup !== undefined
) {
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup =
fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup;
warn(
'subtitleStyle.autoPauseVideoOnYomitanPopup',
(src.subtitleStyle as { autoPauseVideoOnYomitanPopup?: unknown })
.autoPauseVideoOnYomitanPopup,
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup,
'Expected boolean.',
);
}
const hoverTokenColor = asColor(
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
);

View File

@@ -47,6 +47,25 @@ test('subtitleStyle autoPauseVideoOnHover falls back on invalid value', () => {
);
});
test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', () => {
const { context, warnings } = createResolveContext({
subtitleStyle: {
autoPauseVideoOnYomitanPopup: 'invalid' as unknown as boolean,
},
});
applySubtitleDomainConfig(context);
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnYomitanPopup, false);
assert.ok(
warnings.some(
(warning) =>
warning.path === 'subtitleStyle.autoPauseVideoOnYomitanPopup' &&
warning.message === 'Expected boolean.',
),
);
});
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
const valid = createResolveContext({
subtitleStyle: {