mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-03 06:22:41 -08:00
Overlay 2.0 (#12)
This commit is contained in:
@@ -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'));
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user