Overlay 2.0 (#12)

This commit is contained in:
2026-03-01 02:36:51 -08:00
committed by GitHub
parent 45df3c466b
commit 44c7761c7c
397 changed files with 15139 additions and 7127 deletions

View File

@@ -66,3 +66,44 @@ test('warns and falls back for invalid nPlusOne.decks entries', () => {
);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.nPlusOne.decks'));
});
test('accepts valid proxy settings', () => {
const { context, warnings } = makeContext({
proxy: {
enabled: true,
host: '127.0.0.1',
port: 9999,
upstreamUrl: 'http://127.0.0.1:8765',
},
});
applyAnkiConnectResolution(context);
assert.equal(context.resolved.ankiConnect.proxy.enabled, true);
assert.equal(context.resolved.ankiConnect.proxy.host, '127.0.0.1');
assert.equal(context.resolved.ankiConnect.proxy.port, 9999);
assert.equal(context.resolved.ankiConnect.proxy.upstreamUrl, 'http://127.0.0.1:8765');
assert.equal(
warnings.some((warning) => warning.path.startsWith('ankiConnect.proxy')),
false,
);
});
test('warns and falls back for invalid proxy settings', () => {
const { context, warnings } = makeContext({
proxy: {
enabled: 'yes',
host: '',
port: -1,
upstreamUrl: '',
},
});
applyAnkiConnectResolution(context);
assert.deepEqual(context.resolved.ankiConnect.proxy, DEFAULT_CONFIG.ankiConnect.proxy);
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.enabled'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.host'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.port'));
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.upstreamUrl'));
});

View File

@@ -12,6 +12,7 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
const fields = isObject(ac.fields) ? (ac.fields as Record<string, unknown>) : {};
const media = isObject(ac.media) ? (ac.media as Record<string, unknown>) : {};
const metadata = isObject(ac.metadata) ? (ac.metadata as Record<string, unknown>) : {};
const proxy = isObject(ac.proxy) ? (ac.proxy as Record<string, unknown>) : {};
const aiSource = isObject(ac.ai) ? ac.ai : isObject(ac.openRouter) ? ac.openRouter : {};
const legacyKeys = new Set([
'audioField',
@@ -85,6 +86,9 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
? (ac.behavior as (typeof context.resolved)['ankiConnect']['behavior'])
: {}),
},
proxy: {
...context.resolved.ankiConnect.proxy,
},
metadata: {
...context.resolved.ankiConnect.metadata,
...(isObject(ac.metadata)
@@ -153,6 +157,68 @@ export function applyAnkiConnectResolution(context: ResolveContext): void {
);
}
if (isObject(ac.proxy)) {
const proxyEnabled = asBoolean(proxy.enabled);
if (proxyEnabled !== undefined) {
context.resolved.ankiConnect.proxy.enabled = proxyEnabled;
} else if (proxy.enabled !== undefined) {
context.warn(
'ankiConnect.proxy.enabled',
proxy.enabled,
context.resolved.ankiConnect.proxy.enabled,
'Expected boolean.',
);
}
const proxyHost = asString(proxy.host);
if (proxyHost !== undefined && proxyHost.trim().length > 0) {
context.resolved.ankiConnect.proxy.host = proxyHost.trim();
} else if (proxy.host !== undefined) {
context.warn(
'ankiConnect.proxy.host',
proxy.host,
context.resolved.ankiConnect.proxy.host,
'Expected non-empty string.',
);
}
const proxyUpstreamUrl = asString(proxy.upstreamUrl);
if (proxyUpstreamUrl !== undefined && proxyUpstreamUrl.trim().length > 0) {
context.resolved.ankiConnect.proxy.upstreamUrl = proxyUpstreamUrl.trim();
} else if (proxy.upstreamUrl !== undefined) {
context.warn(
'ankiConnect.proxy.upstreamUrl',
proxy.upstreamUrl,
context.resolved.ankiConnect.proxy.upstreamUrl,
'Expected non-empty string.',
);
}
const proxyPort = asNumber(proxy.port);
if (
proxyPort !== undefined &&
Number.isInteger(proxyPort) &&
proxyPort >= 1 &&
proxyPort <= 65535
) {
context.resolved.ankiConnect.proxy.port = proxyPort;
} else if (proxy.port !== undefined) {
context.warn(
'ankiConnect.proxy.port',
proxy.port,
context.resolved.ankiConnect.proxy.port,
'Expected integer between 1 and 65535.',
);
}
} else if (ac.proxy !== undefined) {
context.warn(
'ankiConnect.proxy',
ac.proxy,
context.resolved.ankiConnect.proxy,
'Expected object.',
);
}
if (Array.isArray(ac.tags)) {
const normalizedTags = ac.tags
.filter((entry): entry is string => typeof entry === 'string')

View File

@@ -74,10 +74,33 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
);
}
if (isObject(src.startupWarmups)) {
const startupWarmupBooleanKeys = [
'lowPowerMode',
'mecab',
'yomitanExtension',
'subtitleDictionaries',
'jellyfinRemoteSession',
] as const;
for (const key of startupWarmupBooleanKeys) {
const value = asBoolean(src.startupWarmups[key]);
if (value !== undefined) {
resolved.startupWarmups[key] = value as (typeof resolved.startupWarmups)[typeof key];
} else if (src.startupWarmups[key] !== undefined) {
warn(
`startupWarmups.${key}`,
src.startupWarmups[key],
resolved.startupWarmups[key],
'Expected boolean.',
);
}
}
}
if (isObject(src.shortcuts)) {
const shortcutKeys = [
'toggleVisibleOverlayGlobal',
'toggleInvisibleOverlayGlobal',
'copySubtitle',
'copySubtitleMultiple',
'updateLastCardFromClipboard',
@@ -113,24 +136,6 @@ export function applyCoreDomainConfig(context: ResolveContext): void {
}
}
if (isObject(src.invisibleOverlay)) {
const startupVisibility = src.invisibleOverlay.startupVisibility;
if (
startupVisibility === 'platform-default' ||
startupVisibility === 'visible' ||
startupVisibility === 'hidden'
) {
resolved.invisibleOverlay.startupVisibility = startupVisibility;
} else if (startupVisibility !== undefined) {
warn(
'invisibleOverlay.startupVisibility',
startupVisibility,
resolved.invisibleOverlay.startupVisibility,
'Expected platform-default, visible, or hidden.',
);
}
}
if (isObject(src.secondarySub)) {
if (Array.isArray(src.secondarySub.secondarySubLanguages)) {
resolved.secondarySub.secondarySubLanguages = src.secondarySub.secondarySubLanguages.filter(

View File

@@ -99,10 +99,24 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (isObject(src.subtitleStyle)) {
const fallbackSubtitleStyleEnableJlpt = resolved.subtitleStyle.enableJlpt;
const fallbackSubtitleStylePreserveLineBreaks = resolved.subtitleStyle.preserveLineBreaks;
const fallbackSubtitleStyleAutoPauseVideoOnHover =
resolved.subtitleStyle.autoPauseVideoOnHover;
const fallbackSubtitleStyleHoverTokenColor = resolved.subtitleStyle.hoverTokenColor;
const fallbackSubtitleStyleHoverTokenBackgroundColor =
resolved.subtitleStyle.hoverTokenBackgroundColor;
const fallbackFrequencyDictionary = {
...resolved.subtitleStyle.frequencyDictionary,
};
resolved.subtitleStyle = {
...resolved.subtitleStyle,
...(src.subtitleStyle as ResolvedConfig['subtitleStyle']),
frequencyDictionary: {
...resolved.subtitleStyle.frequencyDictionary,
...(isObject((src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary)
? ((src.subtitleStyle as { frequencyDictionary?: unknown })
.frequencyDictionary as ResolvedConfig['subtitleStyle']['frequencyDictionary'])
: {}),
},
secondary: {
...resolved.subtitleStyle.secondary,
...(isObject(src.subtitleStyle.secondary)
@@ -141,7 +155,27 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const hoverTokenColor = asColor((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor);
const autoPauseVideoOnHover = asBoolean(
(src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover,
);
if (autoPauseVideoOnHover !== undefined) {
resolved.subtitleStyle.autoPauseVideoOnHover = autoPauseVideoOnHover;
} else if (
(src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover !==
undefined
) {
resolved.subtitleStyle.autoPauseVideoOnHover = fallbackSubtitleStyleAutoPauseVideoOnHover;
warn(
'subtitleStyle.autoPauseVideoOnHover',
(src.subtitleStyle as { autoPauseVideoOnHover?: unknown }).autoPauseVideoOnHover,
resolved.subtitleStyle.autoPauseVideoOnHover,
'Expected boolean.',
);
}
const hoverTokenColor = asColor(
(src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor,
);
if (hoverTokenColor !== undefined) {
resolved.subtitleStyle.hoverTokenColor = hoverTokenColor;
} else if ((src.subtitleStyle as { hoverTokenColor?: unknown }).hoverTokenColor !== undefined) {
@@ -154,6 +188,25 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const hoverTokenBackgroundColor = asString(
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor,
);
if (hoverTokenBackgroundColor !== undefined) {
resolved.subtitleStyle.hoverTokenBackgroundColor = hoverTokenBackgroundColor;
} else if (
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor !==
undefined
) {
resolved.subtitleStyle.hoverTokenBackgroundColor =
fallbackSubtitleStyleHoverTokenBackgroundColor;
warn(
'subtitleStyle.hoverTokenBackgroundColor',
(src.subtitleStyle as { hoverTokenBackgroundColor?: unknown }).hoverTokenBackgroundColor,
resolved.subtitleStyle.hoverTokenBackgroundColor,
'Expected a CSS color value (hex, rgba/hsl/hsla, named color, or var()).',
);
}
const frequencyDictionary = isObject(
(src.subtitleStyle as { frequencyDictionary?: unknown }).frequencyDictionary,
)
@@ -166,6 +219,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (frequencyEnabled !== undefined) {
resolved.subtitleStyle.frequencyDictionary.enabled = frequencyEnabled;
} else if ((frequencyDictionary as { enabled?: unknown }).enabled !== undefined) {
resolved.subtitleStyle.frequencyDictionary.enabled = fallbackFrequencyDictionary.enabled;
warn(
'subtitleStyle.frequencyDictionary.enabled',
(frequencyDictionary as { enabled?: unknown }).enabled,
@@ -178,6 +232,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (sourcePath !== undefined) {
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
} else if ((frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined) {
resolved.subtitleStyle.frequencyDictionary.sourcePath =
fallbackFrequencyDictionary.sourcePath;
warn(
'subtitleStyle.frequencyDictionary.sourcePath',
(frequencyDictionary as { sourcePath?: unknown }).sourcePath,
@@ -190,6 +246,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (topX !== undefined && Number.isInteger(topX) && topX > 0) {
resolved.subtitleStyle.frequencyDictionary.topX = Math.floor(topX);
} else if ((frequencyDictionary as { topX?: unknown }).topX !== undefined) {
resolved.subtitleStyle.frequencyDictionary.topX = fallbackFrequencyDictionary.topX;
warn(
'subtitleStyle.frequencyDictionary.topX',
(frequencyDictionary as { topX?: unknown }).topX,
@@ -202,6 +259,7 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (frequencyMode === 'single' || frequencyMode === 'banded') {
resolved.subtitleStyle.frequencyDictionary.mode = frequencyMode;
} else if (frequencyMode !== undefined) {
resolved.subtitleStyle.frequencyDictionary.mode = fallbackFrequencyDictionary.mode;
warn(
'subtitleStyle.frequencyDictionary.mode',
frequencyDictionary.mode,
@@ -210,10 +268,25 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const frequencyMatchMode = (frequencyDictionary as { matchMode?: unknown }).matchMode;
if (frequencyMatchMode === 'headword' || frequencyMatchMode === 'surface') {
resolved.subtitleStyle.frequencyDictionary.matchMode = frequencyMatchMode;
} else if (frequencyMatchMode !== undefined) {
resolved.subtitleStyle.frequencyDictionary.matchMode = fallbackFrequencyDictionary.matchMode;
warn(
'subtitleStyle.frequencyDictionary.matchMode',
frequencyMatchMode,
resolved.subtitleStyle.frequencyDictionary.matchMode,
"Expected 'headword' or 'surface'.",
);
}
const singleColor = asColor((frequencyDictionary as { singleColor?: unknown }).singleColor);
if (singleColor !== undefined) {
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
} else if ((frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined) {
resolved.subtitleStyle.frequencyDictionary.singleColor =
fallbackFrequencyDictionary.singleColor;
warn(
'subtitleStyle.frequencyDictionary.singleColor',
(frequencyDictionary as { singleColor?: unknown }).singleColor,
@@ -228,6 +301,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
if (bandedColors !== undefined) {
resolved.subtitleStyle.frequencyDictionary.bandedColors = bandedColors;
} else if ((frequencyDictionary as { bandedColors?: unknown }).bandedColors !== undefined) {
resolved.subtitleStyle.frequencyDictionary.bandedColors =
fallbackFrequencyDictionary.bandedColors;
warn(
'subtitleStyle.frequencyDictionary.bandedColors',
(frequencyDictionary as { bandedColors?: unknown }).bandedColors,

View File

@@ -27,3 +27,51 @@ test('subtitleStyle preserveLineBreaks falls back while merge is preserved', ()
),
);
});
test('subtitleStyle autoPauseVideoOnHover falls back on invalid value', () => {
const { context, warnings } = createResolveContext({
subtitleStyle: {
autoPauseVideoOnHover: 'invalid' as unknown as boolean,
},
});
applySubtitleDomainConfig(context);
assert.equal(context.resolved.subtitleStyle.autoPauseVideoOnHover, true);
assert.ok(
warnings.some(
(warning) =>
warning.path === 'subtitleStyle.autoPauseVideoOnHover' &&
warning.message === 'Expected boolean.',
),
);
});
test('subtitleStyle frequencyDictionary.matchMode accepts valid values and warns on invalid', () => {
const valid = createResolveContext({
subtitleStyle: {
frequencyDictionary: {
matchMode: 'surface',
},
},
});
applySubtitleDomainConfig(valid.context);
assert.equal(valid.context.resolved.subtitleStyle.frequencyDictionary.matchMode, 'surface');
const invalid = createResolveContext({
subtitleStyle: {
frequencyDictionary: {
matchMode: 'reading' as unknown as 'headword' | 'surface',
},
},
});
applySubtitleDomainConfig(invalid.context);
assert.equal(invalid.context.resolved.subtitleStyle.frequencyDictionary.matchMode, 'headword');
assert.ok(
invalid.warnings.some(
(warning) =>
warning.path === 'subtitleStyle.frequencyDictionary.matchMode' &&
warning.message === "Expected 'headword' or 'surface'.",
),
);
});

View File

@@ -13,16 +13,4 @@ export function applyTopLevelConfig(context: ResolveContext): void {
if (asBoolean(src.auto_start_overlay) !== undefined) {
resolved.auto_start_overlay = src.auto_start_overlay as boolean;
}
if (asBoolean(src.bind_visible_overlay_to_mpv_sub_visibility) !== undefined) {
resolved.bind_visible_overlay_to_mpv_sub_visibility =
src.bind_visible_overlay_to_mpv_sub_visibility as boolean;
} else if (src.bind_visible_overlay_to_mpv_sub_visibility !== undefined) {
warn(
'bind_visible_overlay_to_mpv_sub_visibility',
src.bind_visible_overlay_to_mpv_sub_visibility,
resolved.bind_visible_overlay_to_mpv_sub_visibility,
'Expected boolean.',
);
}
}