fix: Kiku field grouping, frequency particles, sidebar media, Yomitan popup visibility (#91)

This commit is contained in:
2026-05-27 01:40:48 -07:00
committed by GitHub
parent efe50ed1e4
commit 1dcfed86ab
52 changed files with 1695 additions and 368 deletions
+39
View File
@@ -104,6 +104,7 @@ test('loads defaults when config is missing', () => {
assert.equal(config.subtitleStyle.preserveLineBreaks, false);
assert.equal(config.subtitleStyle.autoPauseVideoOnHover, true);
assert.equal(config.subtitleStyle.autoPauseVideoOnYomitanPopup, true);
assert.equal(config.subtitleStyle.primaryVisibleOnYomitanPopup, true);
assert.equal(config.subtitleSidebar.enabled, true);
assert.equal(config.subtitleSidebar.pauseVideoOnHover, true);
assert.equal(config.subtitleStyle.hoverTokenColor, '#f4dbd6');
@@ -545,6 +546,44 @@ test('parses subtitleStyle.autoPauseVideoOnYomitanPopup and warns on invalid val
);
});
test('parses subtitleStyle.primaryVisibleOnYomitanPopup and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(
path.join(validDir, 'config.jsonc'),
`{
"subtitleStyle": {
"primaryVisibleOnYomitanPopup": false
}
}`,
'utf-8',
);
const validService = new ConfigService(validDir);
assert.equal(validService.getConfig().subtitleStyle.primaryVisibleOnYomitanPopup, false);
const invalidDir = makeTempDir();
fs.writeFileSync(
path.join(invalidDir, 'config.jsonc'),
`{
"subtitleStyle": {
"primaryVisibleOnYomitanPopup": "yes"
}
}`,
'utf-8',
);
const invalidService = new ConfigService(invalidDir);
assert.equal(
invalidService.getConfig().subtitleStyle.primaryVisibleOnYomitanPopup,
DEFAULT_CONFIG.subtitleStyle.primaryVisibleOnYomitanPopup,
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'subtitleStyle.primaryVisibleOnYomitanPopup'),
);
});
test('parses subtitleStyle.hoverTokenColor and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(
@@ -8,6 +8,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
preserveLineBreaks: false,
autoPauseVideoOnHover: true,
autoPauseVideoOnYomitanPopup: true,
primaryVisibleOnYomitanPopup: true,
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'transparent',
nameMatchEnabled: false,
@@ -57,6 +57,13 @@ export function buildSubtitleConfigOptionRegistry(
description:
'Automatically pause mpv playback while Yomitan popup is open, then resume when popup closes.',
},
{
path: 'subtitleStyle.primaryVisibleOnYomitanPopup',
kind: 'boolean',
defaultValue: defaultConfig.subtitleStyle.primaryVisibleOnYomitanPopup,
description:
'Keep the primary subtitle bar visible while a Yomitan popup is open when primary subtitles are in hover mode.',
},
{
path: 'subtitleStyle.hoverTokenColor',
kind: 'string',
+23
View File
@@ -186,6 +186,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
const fallbackSubtitleStyleAutoPauseVideoOnHover = resolved.subtitleStyle.autoPauseVideoOnHover;
const fallbackSubtitleStyleAutoPauseVideoOnYomitanPopup =
resolved.subtitleStyle.autoPauseVideoOnYomitanPopup;
const fallbackSubtitleStylePrimaryVisibleOnYomitanPopup =
resolved.subtitleStyle.primaryVisibleOnYomitanPopup;
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
const fallbackSubtitleStyleHoverTokenBackgroundColor =
resolved.subtitleStyle.hoverTokenBackgroundColor;
@@ -333,6 +335,27 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const primaryVisibleOnYomitanPopup = asBoolean(
(src.subtitleStyle as { primaryVisibleOnYomitanPopup?: unknown })
.primaryVisibleOnYomitanPopup,
);
if (primaryVisibleOnYomitanPopup !== undefined) {
resolved.subtitleStyle.primaryVisibleOnYomitanPopup = primaryVisibleOnYomitanPopup;
} else if (
(src.subtitleStyle as { primaryVisibleOnYomitanPopup?: unknown })
.primaryVisibleOnYomitanPopup !== undefined
) {
resolved.subtitleStyle.primaryVisibleOnYomitanPopup =
fallbackSubtitleStylePrimaryVisibleOnYomitanPopup;
warn(
'subtitleStyle.primaryVisibleOnYomitanPopup',
(src.subtitleStyle as { primaryVisibleOnYomitanPopup?: unknown })
.primaryVisibleOnYomitanPopup,
resolved.subtitleStyle.primaryVisibleOnYomitanPopup,
'Expected boolean.',
);
}
const hoverTokenColor = asColor(
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
);
+27
View File
@@ -128,6 +128,33 @@ test('subtitleStyle autoPauseVideoOnYomitanPopup falls back on invalid value', (
);
});
test('subtitleStyle primaryVisibleOnYomitanPopup falls back on invalid value', () => {
const valid = createResolveContext({
subtitleStyle: {
primaryVisibleOnYomitanPopup: false,
},
});
applySubtitleDomainConfig(valid.context);
assert.equal(valid.context.resolved.subtitleStyle.primaryVisibleOnYomitanPopup, false);
const { context, warnings } = createResolveContext({
subtitleStyle: {
primaryVisibleOnYomitanPopup: 'invalid' as unknown as boolean,
},
});
applySubtitleDomainConfig(context);
assert.equal(context.resolved.subtitleStyle.primaryVisibleOnYomitanPopup, true);
assert.ok(
warnings.some(
(warning) =>
warning.path === 'subtitleStyle.primaryVisibleOnYomitanPopup' &&
warning.message === 'Expected boolean.',
),
);
});
test('subtitleStyle primaryDefaultMode accepts valid values and warns on invalid', () => {
const valid = createResolveContext({
subtitleStyle: {
+10 -1
View File
@@ -15,6 +15,8 @@ test('settings registry splits viewing into appearance and behavior categories',
assert.equal(field('subtitleStyle.fontSize').category, 'appearance');
assert.equal(field('subtitleStyle.primaryDefaultMode').category, 'behavior');
assert.equal(field('subtitleStyle.primaryDefaultMode').section, 'Subtitle Behavior');
assert.equal(field('subtitleStyle.primaryVisibleOnYomitanPopup').category, 'behavior');
assert.equal(field('subtitleStyle.primaryVisibleOnYomitanPopup').section, 'Subtitle Behavior');
assert.equal(field('secondarySub.defaultMode').category, 'behavior');
assert.equal(field('subtitlePosition.yPercent').label, 'Subtitle Position');
assert.equal(field('subtitleStyle.frequencyDictionary.mode').label, 'Frequency Mode');
@@ -28,7 +30,14 @@ test('settings registry splits viewing into appearance and behavior categories',
assert.equal(field('mpv.profile').section, 'mpv Playback');
assert.ok(
fields.findIndex((candidate) => candidate.configPath === 'subtitleStyle.primaryDefaultMode') <
fields.findIndex((candidate) => candidate.configPath === 'secondarySub.defaultMode'),
fields.findIndex(
(candidate) => candidate.configPath === 'subtitleStyle.primaryVisibleOnYomitanPopup',
),
);
assert.ok(
fields.findIndex(
(candidate) => candidate.configPath === 'subtitleStyle.primaryVisibleOnYomitanPopup',
) < fields.findIndex((candidate) => candidate.configPath === 'secondarySub.defaultMode'),
);
});
+9 -1
View File
@@ -168,6 +168,7 @@ const PATH_ORDER = new Map<string, number>(
'subtitleStyle.hoverTokenBackgroundColor',
'subtitleStyle.css',
'subtitleStyle.primaryDefaultMode',
'subtitleStyle.primaryVisibleOnYomitanPopup',
'subtitleStyle.secondary.fontColor',
'subtitleStyle.secondary.backgroundColor',
'subtitleStyle.secondary.css',
@@ -218,6 +219,7 @@ const LABEL_OVERRIDES: Record<string, string> = {
'subtitleSidebar.pauseVideoOnHover': 'Pause Video On Hover - Sidebar',
'subtitleStyle.autoPauseVideoOnHover': 'Pause Video On Hover - Subtitles',
'subtitleStyle.autoPauseVideoOnYomitanPopup': 'Pause Video On Yomitan Popup',
'subtitleStyle.primaryVisibleOnYomitanPopup': 'Keep Primary Visible On Yomitan Popup',
'subtitleStyle.primaryDefaultMode': 'Primary Subtitle Visibility Mode',
'subtitleStyle.frequencyDictionary.mode': 'Frequency Mode',
'subtitleStyle.css': 'CSS Declarations',
@@ -251,6 +253,8 @@ const DESCRIPTION_OVERRIDES: Record<string, string> = {
'CSS declarations applied to secondary subtitles. Includes color, background-color, and all font properties.',
'subtitleSidebar.css':
'CSS declarations applied to the subtitle sidebar. Includes color, background-color, all font properties, and sidebar CSS variables.',
'subtitleStyle.primaryVisibleOnYomitanPopup':
'When primary subtitles are in hover mode, keep the primary subtitle bar visible while a Yomitan popup is open.',
'websocket.enabled':
'Built-in subtitle WebSocket server mode. Auto starts the built-in server only when mpv_websocket is not detected; otherwise it defers to the plugin.',
'discordPresence.updateIntervalMs':
@@ -359,7 +363,10 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
if (path.startsWith('subtitleStyle.secondary.')) {
return { category: 'appearance', section: 'Secondary Subtitle Appearance' };
}
if (path === 'subtitleStyle.primaryDefaultMode') {
if (
path === 'subtitleStyle.primaryDefaultMode' ||
path === 'subtitleStyle.primaryVisibleOnYomitanPopup'
) {
return { category: 'behavior', section: 'Subtitle Behavior' };
}
if (path.startsWith('subtitleStyle.')) {
@@ -603,6 +610,7 @@ function isFeatureToggle(field: ConfigSettingsField): boolean {
}
function fieldTypeRank(field: ConfigSettingsField): number {
if (field.configPath === 'subtitleStyle.primaryVisibleOnYomitanPopup') return 2;
if (field.control !== 'boolean') return 2;
return isFeatureToggle(field) ? 0 : 1;
}