feat(config): add configuration window (#70)

This commit is contained in:
2026-05-21 04:16:21 -07:00
committed by GitHub
parent a54f03f0cd
commit dc52bc2fba
287 changed files with 14507 additions and 8134 deletions
+2 -3
View File
@@ -105,7 +105,6 @@ export const CORE_DEFAULT_CONFIG: Pick<
primarySubLanguages: ['ja', 'jpn'],
},
subsync: {
defaultMode: 'auto',
alass_path: '',
ffsubsync_path: '',
ffmpeg_path: '',
@@ -116,7 +115,7 @@ export const CORE_DEFAULT_CONFIG: Pick<
mecab: true,
yomitanExtension: true,
subtitleDictionaries: true,
jellyfinRemoteSession: true,
jellyfinRemoteSession: false,
},
updates: {
enabled: true,
@@ -124,5 +123,5 @@ export const CORE_DEFAULT_CONFIG: Pick<
notificationType: 'system',
channel: 'stable',
},
auto_start_overlay: false,
auto_start_overlay: true,
};
@@ -1,4 +1,5 @@
import { ResolvedConfig } from '../../types/config';
import { getDefaultMpvSocketPath } from '../../shared/mpv-socket-path';
export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
ResolvedConfig,
@@ -59,7 +60,6 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
addMinedWordsImmediately: true,
matchMode: 'headword',
decks: {},
color: '#a6da95',
},
behavior: {
overwriteAudio: true,
@@ -70,15 +70,15 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
autoUpdateNewCards: true,
},
nPlusOne: {
enabled: false,
minSentenceWords: 3,
nPlusOne: '#c6a0f6',
},
metadata: {
pattern: '[SubMiner] %f (%t)',
},
isLapis: {
enabled: false,
sentenceCardModel: 'Japanese sentences',
sentenceCardModel: 'Lapis',
},
isKiku: {
enabled: false,
@@ -94,6 +94,13 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
mpv: {
executablePath: '',
launchMode: 'normal',
socketPath: getDefaultMpvSocketPath(),
backend: 'auto',
autoStartSubMiner: true,
pauseUntilOverlayReady: true,
subminerBinaryPath: '',
aniskipEnabled: true,
aniskipButtonKey: 'TAB',
},
anilist: {
enabled: false,
+12 -5
View File
@@ -3,13 +3,14 @@ import { ResolvedConfig } from '../../types/config';
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
subtitleStyle: {
primaryDefaultMode: 'visible',
css: {},
enableJlpt: false,
preserveLineBreaks: false,
autoPauseVideoOnHover: true,
autoPauseVideoOnYomitanPopup: true,
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'rgba(54, 58, 79, 0.84)',
nameMatchEnabled: true,
hoverTokenBackgroundColor: 'transparent',
nameMatchEnabled: false,
nameMatchColor: '#f5bde6',
fontFamily: 'Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP',
fontSize: 35,
@@ -21,6 +22,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)',
paintOrder: '',
WebkitTextStroke: '',
fontStyle: 'normal',
backgroundColor: 'transparent',
backdropFilter: 'blur(6px)',
@@ -43,7 +46,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
bandedColors: ['#ed8796', '#f5a97f', '#f9e2af', '#8bd5ca', '#8aadf4'],
},
secondary: {
fontFamily: 'Inter, Noto Sans, Helvetica Neue, sans-serif',
css: {},
fontFamily: 'Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP',
fontSize: 24,
fontColor: '#cad3f5',
lineHeight: 1.35,
@@ -52,6 +56,8 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
fontKerning: 'normal',
textRendering: 'geometricPrecision',
textShadow: '0 2px 6px rgba(0,0,0,0.9), 0 0 12px rgba(0,0,0,0.55)',
paintOrder: '',
WebkitTextStroke: '',
backgroundColor: 'transparent',
backdropFilter: 'blur(6px)',
fontWeight: '600',
@@ -63,13 +69,14 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
autoOpen: false,
layout: 'overlay',
toggleKey: 'Backslash',
pauseVideoOnHover: false,
pauseVideoOnHover: true,
autoScroll: true,
css: {},
maxWidth: 420,
opacity: 0.95,
backgroundColor: 'rgba(73, 77, 100, 0.9)',
textColor: '#cad3f5',
fontFamily: '"M PLUS 1", "Noto Sans CJK JP", sans-serif',
fontFamily: 'Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP',
fontSize: 16,
timestampColor: '#a5adcb',
activeLineColor: '#f5bde6',
+25 -2
View File
@@ -63,10 +63,9 @@ const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
'subtitleStyle.jlptColors.N3',
'subtitleStyle.jlptColors.N4',
'subtitleStyle.jlptColors.N5',
'subtitleStyle.knownWordColor',
'subtitleStyle.letterSpacing',
'subtitleStyle.lineHeight',
'subtitleStyle.nPlusOneColor',
'subtitleStyle.paintOrder',
'subtitleStyle.secondary.backdropFilter',
'subtitleStyle.secondary.backgroundColor',
'subtitleStyle.secondary.fontColor',
@@ -77,11 +76,14 @@ const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
'subtitleStyle.secondary.fontWeight',
'subtitleStyle.secondary.letterSpacing',
'subtitleStyle.secondary.lineHeight',
'subtitleStyle.secondary.paintOrder',
'subtitleStyle.secondary.textRendering',
'subtitleStyle.secondary.textShadow',
'subtitleStyle.secondary.WebkitTextStroke',
'subtitleStyle.secondary.wordSpacing',
'subtitleStyle.textRendering',
'subtitleStyle.textShadow',
'subtitleStyle.WebkitTextStroke',
'subtitleStyle.wordSpacing',
]);
@@ -103,6 +105,13 @@ test('config option registry includes critical paths and has unique entries', ()
'anilist.characterDictionary.collapsibleSections.description',
'mpv.executablePath',
'mpv.launchMode',
'mpv.socketPath',
'mpv.backend',
'mpv.autoStartSubMiner',
'mpv.pauseUntilOverlayReady',
'mpv.subminerBinaryPath',
'mpv.aniskipEnabled',
'mpv.aniskipButtonKey',
'yomitan.externalProfilePath',
'immersionTracking.enabled',
]) {
@@ -112,6 +121,20 @@ test('config option registry includes critical paths and has unique entries', ()
assert.equal(new Set(paths).size, paths.length);
});
test('known-word annotation color has one public config path', () => {
const leaves = collectConfigLeafPaths(DEFAULT_CONFIG);
assert.ok(leaves.includes('subtitleStyle.knownWordColor'));
assert.ok(!leaves.includes('ankiConnect.knownWords.color'));
});
test('n+1 annotation color has one public config path', () => {
const leaves = collectConfigLeafPaths(DEFAULT_CONFIG);
assert.ok(leaves.includes('subtitleStyle.nPlusOneColor'));
assert.ok(!leaves.includes('ankiConnect.nPlusOne.color'));
});
test('every DEFAULT_CONFIG leaf is in CONFIG_OPTION_REGISTRY or UNDOCUMENTED_LEAVES', () => {
const registryPaths = new Set(CONFIG_OPTION_REGISTRY.map((entry) => entry.path));
const leaves = collectConfigLeafPaths(DEFAULT_CONFIG);
+2 -8
View File
@@ -339,7 +339,8 @@ export function buildCoreConfigOptionRegistry(
path: 'auto_start_overlay',
kind: 'boolean',
defaultValue: defaultConfig.auto_start_overlay,
description: 'Auto-start the subtitle overlay window when SubMiner launches.',
description:
'Show the visible subtitle overlay automatically when the bundled mpv plugin starts SubMiner.',
},
{
path: 'secondarySub.secondarySubLanguages',
@@ -387,13 +388,6 @@ export function buildCoreConfigOptionRegistry(
defaultValue: defaultConfig.annotationWebsocket.port,
description: 'Annotated subtitle websocket server port.',
},
{
path: 'subsync.defaultMode',
kind: 'enum',
enumValues: ['auto', 'manual'],
defaultValue: defaultConfig.subsync.defaultMode,
description: 'Subsync default mode.',
},
{
path: 'subsync.replace',
kind: 'boolean',
+56 -13
View File
@@ -278,6 +278,13 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.ankiConnect.knownWords.addMinedWordsImmediately,
description: 'Immediately append newly mined card words into the known-word cache.',
},
{
path: 'ankiConnect.nPlusOne.enabled',
kind: 'boolean',
defaultValue: defaultConfig.ankiConnect.nPlusOne.enabled,
description:
'Enable N+1 subtitle highlighting (highlights the one unknown word in a sentence). Requires known-word cache data.',
},
{
path: 'ankiConnect.nPlusOne.minSentenceWords',
kind: 'number',
@@ -291,18 +298,6 @@ export function buildIntegrationConfigOptionRegistry(
description:
'Decks and fields for known-word cache. Object mapping deck names to arrays of field names to extract, e.g. { "Kaishi 1.5k": ["Word", "Word Reading"] }.',
},
{
path: 'ankiConnect.nPlusOne.nPlusOne',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.nPlusOne.nPlusOne,
description: 'Color used for the single N+1 target token highlight.',
},
{
path: 'ankiConnect.knownWords.color',
kind: 'string',
defaultValue: defaultConfig.ankiConnect.knownWords.color,
description: 'Color used for known-word highlights.',
},
{
path: 'ankiConnect.isKiku.fieldGrouping',
kind: 'enum',
@@ -454,6 +449,53 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.mpv.launchMode,
description: 'Default window state for SubMiner-managed mpv launches.',
},
{
path: 'mpv.socketPath',
kind: 'string',
defaultValue: defaultConfig.mpv.socketPath,
description:
'mpv IPC socket path used by SubMiner-managed playback and the bundled mpv plugin.',
},
{
path: 'mpv.backend',
kind: 'enum',
enumValues: ['auto', 'hyprland', 'sway', 'x11', 'macos', 'windows'],
defaultValue: defaultConfig.mpv.backend,
description:
'Window tracking backend passed to the bundled mpv plugin. Auto detects the current platform.',
},
{
path: 'mpv.autoStartSubMiner',
kind: 'boolean',
defaultValue: defaultConfig.mpv.autoStartSubMiner,
description: 'Start SubMiner in the background when SubMiner-managed mpv loads a file.',
},
{
path: 'mpv.pauseUntilOverlayReady',
kind: 'boolean',
defaultValue: defaultConfig.mpv.pauseUntilOverlayReady,
description:
'Pause mpv on visible-overlay auto-start until SubMiner signals subtitle tokenization readiness.',
},
{
path: 'mpv.subminerBinaryPath',
kind: 'string',
defaultValue: defaultConfig.mpv.subminerBinaryPath,
description:
'Optional SubMiner app binary path passed to the bundled mpv plugin. Leave empty to use the launcher-detected app path.',
},
{
path: 'mpv.aniskipEnabled',
kind: 'boolean',
defaultValue: defaultConfig.mpv.aniskipEnabled,
description: 'Enable AniSkip intro detection and skip markers in the bundled mpv plugin.',
},
{
path: 'mpv.aniskipButtonKey',
kind: 'string',
defaultValue: defaultConfig.mpv.aniskipButtonKey,
description: 'mpv key used to trigger the AniSkip button while the skip marker is visible.',
},
{
path: 'jellyfin.enabled',
kind: 'boolean',
@@ -567,7 +609,8 @@ export function buildIntegrationConfigOptionRegistry(
},
{
path: 'discordPresence.presenceStyle',
kind: 'string',
kind: 'enum',
enumValues: ['default', 'meme', 'japanese', 'minimal'],
defaultValue: defaultConfig.discordPresence.presenceStyle,
description:
'Presence card text preset: "default" (clean bilingual), "meme" (Mining and crafting), "japanese" (fully JP), or "minimal".',
@@ -13,6 +13,20 @@ export function buildSubtitleConfigOptionRegistry(
description:
'Default primary subtitle bar visibility mode. hidden hides it, visible shows it, hover reveals it on hover.',
},
{
path: 'subtitleStyle.css',
kind: 'object',
defaultValue: defaultConfig.subtitleStyle.css,
description:
'CSS declaration object applied to primary subtitles after normal subtitle style defaults.',
},
{
path: 'subtitleStyle.secondary.css',
kind: 'object',
defaultValue: defaultConfig.subtitleStyle.secondary.css,
description:
'CSS declaration object applied to secondary subtitles after normal subtitle style defaults.',
},
{
path: 'subtitleStyle.enableJlpt',
kind: 'boolean',
@@ -69,6 +83,18 @@ export function buildSubtitleConfigOptionRegistry(
description:
'Hex color used when a subtitle token matches an entry from the SubMiner character dictionary.',
},
{
path: 'subtitleStyle.knownWordColor',
kind: 'string',
defaultValue: defaultConfig.subtitleStyle.knownWordColor,
description: 'Color used for known-word subtitle highlights.',
},
{
path: 'subtitleStyle.nPlusOneColor',
kind: 'string',
defaultValue: defaultConfig.subtitleStyle.nPlusOneColor,
description: 'Color used for the single N+1 target token subtitle highlight.',
},
{
path: 'subtitleStyle.frequencyDictionary.enabled',
kind: 'boolean',
@@ -155,6 +181,13 @@ export function buildSubtitleConfigOptionRegistry(
defaultValue: defaultConfig.subtitleSidebar.autoScroll,
description: 'Auto-scroll the active subtitle cue into view while playback advances.',
},
{
path: 'subtitleSidebar.css',
kind: 'object',
defaultValue: defaultConfig.subtitleSidebar.css,
description:
'CSS declaration object applied to the subtitle sidebar. Includes color, background-color, and all font properties.',
},
{
path: 'subtitleSidebar.maxWidth',
kind: 'number',
+18 -2
View File
@@ -20,9 +20,9 @@ export function buildRuntimeOptionRegistry(
}),
},
{
id: 'subtitle.annotation.nPlusOne',
id: 'subtitle.annotation.knownWords.highlightEnabled',
path: 'ankiConnect.knownWords.highlightEnabled',
label: 'N+1 Annotation',
label: 'Known Word Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
@@ -35,6 +35,22 @@ export function buildRuntimeOptionRegistry(
},
}),
},
{
id: 'subtitle.annotation.nPlusOne',
path: 'ankiConnect.nPlusOne.enabled',
label: 'N+1 Annotation',
scope: 'subtitle',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.nPlusOne.enabled,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: (value) => ({
nPlusOne: {
enabled: value === true,
},
}),
},
{
id: 'subtitle.annotation.jlpt',
path: 'subtitleStyle.enableJlpt',
+12 -5
View File
@@ -2,9 +2,10 @@ import { ConfigTemplateSection } from './shared';
const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{
title: 'Overlay Auto-Start',
title: 'Visible Overlay Auto-Start',
description: [
'When overlay connects to mpv, automatically show overlay and hide mpv subtitles.',
'Show the visible subtitle overlay automatically after managed mpv playback starts SubMiner.',
'SubMiner can still auto-start in the background when this is false.',
],
key: 'auto_start_overlay',
},
@@ -32,6 +33,7 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{
title: 'Logging',
description: ['Controls logging verbosity.', 'Set to debug for full runtime diagnostics.'],
notes: ['Hot-reload: logging.level applies live while SubMiner is running.'],
key: 'logging',
},
{
@@ -88,8 +90,9 @@ const CORE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
key: 'secondarySub',
},
{
title: 'Auto Subtitle Sync',
title: 'Subtitle Sync',
description: ['Subsync engine and executable paths.'],
notes: ['Hot-reload: subsync changes apply to the next subtitle sync run.'],
key: 'subsync',
},
{
@@ -126,7 +129,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
title: 'AnkiConnect Integration',
description: ['Automatic Anki updates and media generation options.'],
notes: [
'Hot-reload: ankiConnect.ai.enabled updates live while SubMiner is running.',
'Hot-reload: ankiConnect.ai.enabled, knownWords, nPlusOne, fields.word/audio/image/sentence/miscInfo, behavior.autoUpdateNewCards, isLapis.sentenceCardModel, and isKiku.fieldGrouping update live while SubMiner is running.',
'Shared AI provider transport settings are read from top-level ai and typically require restart.',
'Most other AnkiConnect settings still require restart.',
],
@@ -135,6 +138,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{
title: 'Jimaku',
description: ['Jimaku API configuration and defaults.'],
notes: ['Hot-reload: Jimaku changes apply to the next Jimaku request.'],
key: 'jimaku',
},
{
@@ -142,6 +146,7 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
description: [
'Defaults for managed subtitle language preferences and YouTube subtitle loading.',
],
notes: ['Hot-reload: primarySubLanguages applies to the next YouTube subtitle load.'],
key: 'youtube',
},
{
@@ -166,7 +171,9 @@ const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
{
title: 'MPV Launcher',
description: [
'Optional mpv.exe override for Windows playback entry points.',
'SubMiner-managed mpv launch and bundled plugin options.',
'Set mpv.socketPath to the IPC socket used by the launcher, Electron app, and bundled plugin.',
'autoStartSubMiner starts SubMiner in the background; auto_start_overlay only controls visible overlay display.',
'Set mpv.launchMode to choose normal, maximized, or fullscreen SubMiner-managed mpv playback.',
'Leave mpv.executablePath blank to auto-discover mpv.exe from SUBMINER_MPV_PATH or PATH.',
],