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

@@ -10,9 +10,8 @@ export const CORE_DEFAULT_CONFIG: Pick<
| 'shortcuts'
| 'secondarySub'
| 'subsync'
| 'startupWarmups'
| 'auto_start_overlay'
| 'bind_visible_overlay_to_mpv_sub_visibility'
| 'invisibleOverlay'
> = {
subtitlePosition: { yPercent: 10 },
keybindings: [],
@@ -28,7 +27,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
},
shortcuts: {
toggleVisibleOverlayGlobal: 'Alt+Shift+O',
toggleInvisibleOverlayGlobal: 'Alt+Shift+I',
copySubtitle: 'CommandOrControl+C',
copySubtitleMultiple: 'CommandOrControl+Shift+C',
updateLastCardFromClipboard: 'CommandOrControl+V',
@@ -53,9 +51,12 @@ export const CORE_DEFAULT_CONFIG: Pick<
ffsubsync_path: '',
ffmpeg_path: '',
},
auto_start_overlay: false,
bind_visible_overlay_to_mpv_sub_visibility: true,
invisibleOverlay: {
startupVisibility: 'platform-default',
startupWarmups: {
lowPowerMode: false,
mecab: true,
yomitanExtension: true,
subtitleDictionaries: true,
jellyfinRemoteSession: true,
},
auto_start_overlay: false,
};

View File

@@ -8,6 +8,12 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
enabled: false,
url: 'http://127.0.0.1:8765',
pollingRate: 3000,
proxy: {
enabled: true,
host: '127.0.0.1',
port: 8766,
upstreamUrl: 'http://127.0.0.1:8765',
},
tags: ['SubMiner'],
fields: {
audio: 'ExpressionAudio',

View File

@@ -4,14 +4,22 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
subtitleStyle: {
enableJlpt: false,
preserveLineBreaks: false,
hoverTokenColor: '#c6a0f6',
fontFamily:
'M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif',
autoPauseVideoOnHover: true,
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
fontFamily: 'M PLUS 1 Medium, Source Han Sans JP, Noto Sans CJK JP',
fontSize: 35,
fontColor: '#cad3f5',
fontWeight: 'normal',
fontWeight: '600',
lineHeight: 1.35,
letterSpacing: '-0.01em',
wordSpacing: 0,
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 3px 10px rgba(0,0,0,0.69)',
fontStyle: 'normal',
backgroundColor: 'rgb(30, 32, 48, 0.88)',
backdropFilter: 'blur(6px)',
nPlusOneColor: '#c6a0f6',
knownWordColor: '#a6da95',
jlptColors: {
@@ -26,17 +34,24 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
sourcePath: '',
topX: 1000,
mode: 'single',
matchMode: 'headword',
singleColor: '#f5a97f',
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#a6e3a1', '#8aadf4'],
},
secondary: {
fontFamily: 'Inter, Noto Sans, Helvetica Neue, sans-serif',
fontSize: 24,
fontColor: '#ffffff',
fontColor: '#cad3f5',
lineHeight: 1.35,
letterSpacing: '-0.01em',
wordSpacing: 0,
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 3px 10px rgba(0,0,0,0.69)',
backgroundColor: 'transparent',
backdropFilter: 'blur(6px)',
fontWeight: 'normal',
fontStyle: 'normal',
fontFamily:
'M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif',
},
},
};

View File

@@ -5,6 +5,7 @@ import {
CONFIG_OPTION_REGISTRY,
CONFIG_TEMPLATE_SECTIONS,
DEFAULT_CONFIG,
DEFAULT_KEYBINDINGS,
RUNTIME_OPTION_REGISTRY,
} from '../definitions';
import { buildCoreConfigOptionRegistry } from './options-core';
@@ -17,6 +18,7 @@ test('config option registry includes critical paths and has unique entries', ()
for (const requiredPath of [
'logging.level',
'startupWarmups.lowPowerMode',
'subtitleStyle.enableJlpt',
'ankiConnect.enabled',
'immersionTracking.enabled',
@@ -31,6 +33,7 @@ test('config template sections include expected domains and unique keys', () =>
const keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key);
const requiredKeys: (typeof keys)[number][] = [
'websocket',
'startupWarmups',
'subtitleStyle',
'ankiConnect',
'immersionTracking',
@@ -57,3 +60,11 @@ test('domain registry builders each contribute entries to composed registry', ()
assert.ok(entries.some((entry) => composedPaths.has(entry.path)));
}
});
test('default keybindings include primary and secondary subtitle track cycling on J keys', () => {
const keybindingMap = new Map(
DEFAULT_KEYBINDINGS.map((binding) => [binding.key, binding.command]),
);
assert.deepEqual(keybindingMap.get('KeyJ'), ['cycle', 'sid']);
assert.deepEqual(keybindingMap.get('Shift+KeyJ'), ['cycle', 'secondary-sid']);
});

View File

@@ -32,18 +32,41 @@ export function buildCoreConfigOptionRegistry(
defaultValue: defaultConfig.subsync.defaultMode,
description: 'Subsync default mode.',
},
{
path: 'startupWarmups.lowPowerMode',
kind: 'boolean',
defaultValue: defaultConfig.startupWarmups.lowPowerMode,
description: 'Defer startup warmups except Yomitan extension.',
},
{
path: 'startupWarmups.mecab',
kind: 'boolean',
defaultValue: defaultConfig.startupWarmups.mecab,
description: 'Warm up MeCab tokenizer at startup.',
},
{
path: 'startupWarmups.yomitanExtension',
kind: 'boolean',
defaultValue: defaultConfig.startupWarmups.yomitanExtension,
description: 'Warm up Yomitan extension at startup.',
},
{
path: 'startupWarmups.subtitleDictionaries',
kind: 'boolean',
defaultValue: defaultConfig.startupWarmups.subtitleDictionaries,
description: 'Warm up subtitle dictionaries at startup.',
},
{
path: 'startupWarmups.jellyfinRemoteSession',
kind: 'boolean',
defaultValue: defaultConfig.startupWarmups.jellyfinRemoteSession,
description: 'Warm up Jellyfin remote session at startup.',
},
{
path: 'shortcuts.multiCopyTimeoutMs',
kind: 'number',
defaultValue: defaultConfig.shortcuts.multiCopyTimeoutMs,
description: 'Timeout for multi-copy/mine modes.',
},
{
path: 'bind_visible_overlay_to_mpv_sub_visibility',
kind: 'boolean',
defaultValue: defaultConfig.bind_visible_overlay_to_mpv_sub_visibility,
description:
'Link visible overlay toggles to MPV subtitle visibility (primary and secondary).',
},
];
}

View File

@@ -5,6 +5,8 @@ export function buildIntegrationConfigOptionRegistry(
defaultConfig: ResolvedConfig,
runtimeOptionRegistry: RuntimeOptionRegistryEntry[],
): ConfigOptionRegistryEntry[] {
const runtimeOptionById = new Map(runtimeOptionRegistry.map((entry) => [entry.id, entry]));
return [
{
path: 'ankiConnect.enabled',
@@ -18,6 +20,30 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.ankiConnect.pollingRate,
description: 'Polling interval in milliseconds.',
},
{
path: 'ankiConnect.proxy.enabled',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.proxy.enabled,
description: 'Enable local AnkiConnect-compatible proxy for push-based auto-enrichment.',
},
{
path: 'ankiConnect.proxy.host',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.proxy.host,
description: 'Bind host for local AnkiConnect proxy.',
},
{
path: 'ankiConnect.proxy.port',
kind: 'number',
defaultValue: defaultConfig.ankiConnect.proxy.port,
description: 'Bind port for local AnkiConnect proxy.',
},
{
path: 'ankiConnect.proxy.upstreamUrl',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.proxy.upstreamUrl,
description: 'Upstream AnkiConnect URL proxied by local AnkiConnect proxy.',
},
{
path: 'ankiConnect.tags',
kind: 'array',
@@ -30,7 +56,7 @@ export function buildIntegrationConfigOptionRegistry(
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.behavior.autoUpdateNewCards,
description: 'Automatically update newly added cards.',
runtime: runtimeOptionRegistry[0],
runtime: runtimeOptionById.get('anki.autoUpdateNewCards'),
},
{
path: 'ankiConnect.nPlusOne.matchMode',
@@ -81,7 +107,7 @@ export function buildIntegrationConfigOptionRegistry(
enumValues: ['auto', 'manual', 'disabled'],
defaultValue: defaultConfig.ankiConnect.isKiku.fieldGrouping,
description: 'Kiku duplicate-card field grouping mode.',
runtime: runtimeOptionRegistry[1],
runtime: runtimeOptionById.get('anki.kikuFieldGrouping'),
},
{
path: 'jimaku.languagePreference',

View File

@@ -21,12 +21,25 @@ export function buildSubtitleConfigOptionRegistry(
'Preserve line breaks in visible overlay subtitle rendering. ' +
'When false, line breaks are flattened to spaces for a single-line flow.',
},
{
path: 'subtitleStyle.autoPauseVideoOnHover',
kind: 'boolean',
defaultValue: defaultConfig.subtitleStyle.autoPauseVideoOnHover,
description:
'Automatically pause mpv playback while hovering subtitle text, then resume on leave.',
},
{
path: 'subtitleStyle.hoverTokenColor',
kind: 'string',
defaultValue: defaultConfig.subtitleStyle.hoverTokenColor,
description: 'Hex color used for hovered subtitle token highlight in mpv.',
},
{
path: 'subtitleStyle.hoverTokenBackgroundColor',
kind: 'string',
defaultValue: defaultConfig.subtitleStyle.hoverTokenBackgroundColor,
description: 'CSS color used for hovered subtitle token background highlight in mpv.',
},
{
path: 'subtitleStyle.frequencyDictionary.enabled',
kind: 'boolean',
@@ -55,6 +68,14 @@ export function buildSubtitleConfigOptionRegistry(
description:
'single: use one color for all matching tokens. banded: use color ramp by frequency band.',
},
{
path: 'subtitleStyle.frequencyDictionary.matchMode',
kind: 'enum',
enumValues: ['headword', 'surface'],
defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.matchMode,
description:
'headword: frequency lookup uses dictionary form. surface: lookup uses subtitle-visible token text.',
},
{
path: 'subtitleStyle.frequencyDictionary.singleColor',
kind: 'string',

View File

@@ -19,6 +19,42 @@ export function buildRuntimeOptionRegistry(
behavior: { autoUpdateNewCards: value === true },
}),
},
{
id: 'subtitle.annotation.nPlusOne',
path: 'ankiConnect.nPlusOne.highlightEnabled',
label: 'N+1 Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.nPlusOne.highlightEnabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{
id: 'subtitle.annotation.jlpt',
path: 'subtitleStyle.enableJlpt',
label: 'JLPT Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.subtitleStyle.enableJlpt,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{
id: 'subtitle.annotation.frequency',
path: 'subtitleStyle.frequencyDictionary.enabled',
label: 'Frequency Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.subtitleStyle.frequencyDictionary.enabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: () => ({}),
},
{
id: 'anki.nPlusOneMatchMode',
path: 'ankiConnect.nPlusOne.matchMode',

View File

@@ -48,6 +48,8 @@ export const SPECIAL_COMMANDS = {
export const DEFAULT_KEYBINDINGS: NonNullable<ResolvedConfig['keybindings']> = [
{ key: 'Space', command: ['cycle', 'pause'] },
{ key: 'KeyJ', command: ['cycle', 'sid'] },
{ key: 'Shift+KeyJ', command: ['cycle', 'secondary-sid'] },
{ key: 'ArrowRight', command: ['seek', 5] },
{ key: 'ArrowLeft', command: ['seek', -5] },
{ key: 'ArrowUp', command: ['seek', 60] },

View File

@@ -8,14 +8,6 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
],
key: 'auto_start_overlay',
},
{
title: 'Visible Overlay Subtitle Binding',
description: [
'Control whether visible overlay toggles also toggle MPV subtitle visibility.',
'When enabled, visible overlay hides MPV subtitles; when disabled, MPV subtitles are left unchanged.',
],
key: 'bind_visible_overlay_to_mpv_sub_visibility',
},
{
title: 'Texthooker Server',
description: ['Control whether browser opens automatically for texthooker.'],
@@ -34,21 +26,21 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
key: 'logging',
},
{
title: 'Startup Warmups',
description: [
'Background warmup controls for MeCab, Yomitan, dictionaries, and Jellyfin session.',
'Disable individual warmups to defer load until first real usage.',
'lowPowerMode defers all warmups except Yomitan extension.',
],
key: 'startupWarmups',
},
{
title: 'Keyboard Shortcuts',
description: ['Overlay keyboard shortcuts. Set a shortcut to null to disable.'],
notes: ['Hot-reload: shortcut changes apply live and update the session help modal on reopen.'],
key: 'shortcuts',
},
{
title: 'Invisible Overlay',
description: ['Startup behavior for the invisible interactive subtitle mining layer.'],
notes: [
'Invisible subtitle position edit mode: Ctrl/Cmd+Shift+P to toggle, arrow keys to move, Enter or Ctrl/Cmd+S to save, Esc to cancel.',
'This edit-mode shortcut is fixed and is not currently configurable.',
],
key: 'invisibleOverlay',
},
{
title: 'Keybindings (MPV Commands)',
description: [