mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-21 12:11:28 -07:00
feat(subtitle-sidebar): add sidebar config surface
This commit is contained in:
5
changes/2026-03-20-subtitle-sidebar-stability.md
Normal file
5
changes/2026-03-20-subtitle-sidebar-stability.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Kept subtitle sidebar cue tracking stable across transitions by avoiding cue-line regression on subtitle timing edge cases and stale text updates.
|
||||||
|
- Improved sidebar config by documenting and exposing layout mode and typography options (`layout`, `fontFamily`, `fontSize`) in the generated documentation flow.
|
||||||
@@ -284,6 +284,29 @@
|
|||||||
} // Secondary setting.
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Subtitle Sidebar
|
||||||
|
// Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
// Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.
|
||||||
|
// ==========================================
|
||||||
|
"subtitleSidebar": {
|
||||||
|
"enabled": false, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||||
|
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv.
|
||||||
|
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||||
|
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
||||||
|
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
||||||
|
"maxWidth": 420, // Maximum sidebar width in CSS pixels.
|
||||||
|
"opacity": 0.95, // Base opacity applied to the sidebar shell.
|
||||||
|
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell.
|
||||||
|
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar.
|
||||||
|
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text.
|
||||||
|
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels.
|
||||||
|
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar.
|
||||||
|
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue.
|
||||||
|
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue.
|
||||||
|
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues.
|
||||||
|
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Shared AI Provider
|
// Shared AI Provider
|
||||||
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ SubMiner watches the active config file (`config.jsonc` or `config.json`) while
|
|||||||
Hot-reloadable fields:
|
Hot-reloadable fields:
|
||||||
|
|
||||||
- `subtitleStyle`
|
- `subtitleStyle`
|
||||||
|
- `subtitleSidebar`
|
||||||
- `keybindings`
|
- `keybindings`
|
||||||
- `shortcuts`
|
- `shortcuts`
|
||||||
- `secondarySub.defaultMode`
|
- `secondarySub.defaultMode`
|
||||||
@@ -88,6 +89,7 @@ The configuration file includes several main sections:
|
|||||||
**Subtitle Display**
|
**Subtitle Display**
|
||||||
|
|
||||||
- [**Subtitle Style**](#subtitle-style) - Appearance customization
|
- [**Subtitle Style**](#subtitle-style) - Appearance customization
|
||||||
|
- [**Subtitle Sidebar**](#subtitle-sidebar) - Parsed cue list sidebar modal
|
||||||
- [**Subtitle Position**](#subtitle-position) - Overlay vertical positioning
|
- [**Subtitle Position**](#subtitle-position) - Overlay vertical positioning
|
||||||
- [**Secondary Subtitles**](#secondary-subtitles) - Dual subtitle track support
|
- [**Secondary Subtitles**](#secondary-subtitles) - Dual subtitle track support
|
||||||
|
|
||||||
@@ -337,6 +339,46 @@ Secondary subtitle defaults: `fontFamily: "Inter, Noto Sans, Helvetica Neue, san
|
|||||||
|
|
||||||
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
|
**See `config.example.jsonc`** for the complete list of subtitle style configuration options.
|
||||||
|
|
||||||
|
### Subtitle Sidebar
|
||||||
|
|
||||||
|
Configure the parsed-subtitle sidebar modal.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"subtitleSidebar": {
|
||||||
|
"enabled": true,
|
||||||
|
"layout": "overlay",
|
||||||
|
"toggleKey": "Backslash",
|
||||||
|
"pauseVideoOnHover": false,
|
||||||
|
"autoScroll": true,
|
||||||
|
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif",
|
||||||
|
"fontSize": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Values | Description |
|
||||||
|
| --------------------------- | ---------------- | -------------------------------------------------------------------------------- |
|
||||||
|
| `enabled` | boolean | Enable subtitle sidebar support (`false` by default) |
|
||||||
|
| `layout` | string | `"overlay"` floats over mpv; `"embedded"` reserves right-side player space to mimic browser-like layout |
|
||||||
|
| `toggleKey` | string | `KeyboardEvent.code` used to open/close the sidebar (default: `"Backslash"`) |
|
||||||
|
| `pauseVideoOnHover` | boolean | Pause playback while hovering the sidebar cue list |
|
||||||
|
| `autoScroll` | boolean | Keep the active cue in view while playback advances |
|
||||||
|
| `maxWidth` | number | Maximum sidebar width in CSS pixels (default: `420`) |
|
||||||
|
| `opacity` | number | Sidebar opacity between `0` and `1` (default: `0.78`) |
|
||||||
|
| `backgroundColor` | string | Sidebar shell background color |
|
||||||
|
| `textColor` | hex color | Default cue text color |
|
||||||
|
| `fontFamily` | string | CSS `font-family` value applied to sidebar cue text |
|
||||||
|
| `fontSize` | number | Base sidebar cue font size in CSS pixels (default: `16`) |
|
||||||
|
| `timestampColor` | hex color | Cue timestamp color |
|
||||||
|
| `activeLineColor` | hex color | Active cue text color |
|
||||||
|
| `activeLineBackgroundColor` | string | Active cue background color |
|
||||||
|
| `hoverLineBackgroundColor` | string | Hovered cue background color |
|
||||||
|
|
||||||
|
The sidebar is only available when the active subtitle source has been parsed into a cue list. Default colors use Catppuccin Macchiato with a semi-transparent shell so the panel stays readable without feeling like an opaque settings dialog.
|
||||||
|
|
||||||
|
`embedded` layout is intended to act like a split-pane view: it reserves player space with a right-side video margin and keeps interaction in both the player area and sidebar. If you see unexpected offset behavior in your environment, switch back to `overlay` to isolate sidebar placement.
|
||||||
|
|
||||||
`jlptColors` keys are:
|
`jlptColors` keys are:
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|
|||||||
@@ -284,6 +284,29 @@
|
|||||||
} // Secondary setting.
|
} // Secondary setting.
|
||||||
}, // Primary and secondary subtitle styling.
|
}, // Primary and secondary subtitle styling.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Subtitle Sidebar
|
||||||
|
// Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
// Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.
|
||||||
|
// ==========================================
|
||||||
|
"subtitleSidebar": {
|
||||||
|
"enabled": false, // Enable the subtitle sidebar feature for parsed subtitle sources. Values: true | false
|
||||||
|
"layout": "overlay", // Render the subtitle sidebar as a floating overlay or reserve space inside mpv.
|
||||||
|
"toggleKey": "Backslash", // KeyboardEvent.code used to toggle the subtitle sidebar open and closed.
|
||||||
|
"pauseVideoOnHover": false, // Pause mpv while hovering the subtitle sidebar, then resume on leave. Values: true | false
|
||||||
|
"autoScroll": true, // Auto-scroll the active subtitle cue into view while playback advances. Values: true | false
|
||||||
|
"maxWidth": 420, // Maximum sidebar width in CSS pixels.
|
||||||
|
"opacity": 0.95, // Base opacity applied to the sidebar shell.
|
||||||
|
"backgroundColor": "rgba(73, 77, 100, 0.9)", // Background color for the subtitle sidebar shell.
|
||||||
|
"textColor": "#cad3f5", // Default cue text color in the subtitle sidebar.
|
||||||
|
"fontFamily": "\"M PLUS 1\", \"Noto Sans CJK JP\", sans-serif", // Font family used for subtitle sidebar cue text.
|
||||||
|
"fontSize": 16, // Base font size for subtitle sidebar cue text in CSS pixels.
|
||||||
|
"timestampColor": "#a5adcb", // Timestamp color in the subtitle sidebar.
|
||||||
|
"activeLineColor": "#f5bde6", // Text color for the active subtitle cue.
|
||||||
|
"activeLineBackgroundColor": "rgba(138, 173, 244, 0.22)", // Background color for the active subtitle cue.
|
||||||
|
"hoverLineBackgroundColor": "rgba(54, 58, 79, 0.84)" // Background color for hovered subtitle cues.
|
||||||
|
}, // Parsed-subtitle sidebar cue list styling, behavior, and toggle key.
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Shared AI Provider
|
// Shared AI Provider
|
||||||
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
// Canonical OpenAI-compatible provider transport settings shared by Anki and YouTube subtitle fixing.
|
||||||
|
|||||||
@@ -68,10 +68,13 @@ Mouse-hover playback behavior is configured separately from shortcuts: `subtitle
|
|||||||
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
| `Ctrl/Cmd+Shift+O` | Open runtime options palette | `shortcuts.openRuntimeOptions` |
|
||||||
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
| `Ctrl+Shift+J` | Open Jimaku subtitle search modal | `shortcuts.openJimaku` |
|
||||||
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
| `Ctrl+Alt+S` | Open subtitle sync (subsync) modal | `shortcuts.triggerSubsync` |
|
||||||
|
| `\` | Toggle subtitle sidebar | `subtitleSidebar.toggleKey` |
|
||||||
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
| `` ` `` | Toggle stats overlay | `stats.toggleKey` |
|
||||||
|
|
||||||
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
The stats toggle is handled inside the focused visible overlay window. It is configurable through the top-level `stats.toggleKey` setting and defaults to `Backquote`.
|
||||||
|
|
||||||
|
The subtitle sidebar toggle is overlay-local and only opens when SubMiner has a parsed cue list for the active subtitle source.
|
||||||
|
|
||||||
## Controller Shortcuts
|
## Controller Shortcuts
|
||||||
|
|
||||||
These overlay-local shortcuts are fixed and open controller utilities for the Chrome Gamepad API integration.
|
These overlay-local shortcuts are fixed and open controller utilities for the Chrome Gamepad API integration.
|
||||||
@@ -133,4 +136,4 @@ The `keybindings` array overrides or extends the overlay's built-in key handling
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Both `shortcuts` and `keybindings` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
Both `shortcuts`, `keybindings`, and `subtitleSidebar` are [hot-reloadable](/configuration#hot-reload-behavior) — changes take effect without restarting SubMiner.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const {
|
|||||||
} = CORE_DEFAULT_CONFIG;
|
} = CORE_DEFAULT_CONFIG;
|
||||||
const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
|
const { ankiConnect, jimaku, anilist, yomitan, jellyfin, discordPresence, ai, youtubeSubgen } =
|
||||||
INTEGRATIONS_DEFAULT_CONFIG;
|
INTEGRATIONS_DEFAULT_CONFIG;
|
||||||
const { subtitleStyle } = SUBTITLE_DEFAULT_CONFIG;
|
const { subtitleStyle, subtitleSidebar } = SUBTITLE_DEFAULT_CONFIG;
|
||||||
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
const { immersionTracking } = IMMERSION_DEFAULT_CONFIG;
|
||||||
const { stats } = STATS_DEFAULT_CONFIG;
|
const { stats } = STATS_DEFAULT_CONFIG;
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
|
|||||||
subsync,
|
subsync,
|
||||||
startupWarmups,
|
startupWarmups,
|
||||||
subtitleStyle,
|
subtitleStyle,
|
||||||
|
subtitleSidebar,
|
||||||
auto_start_overlay,
|
auto_start_overlay,
|
||||||
jimaku,
|
jimaku,
|
||||||
anilist,
|
anilist,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ResolvedConfig } from '../../types';
|
import { ResolvedConfig } from '../../types';
|
||||||
|
|
||||||
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
|
export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'subtitleSidebar'> = {
|
||||||
subtitleStyle: {
|
subtitleStyle: {
|
||||||
enableJlpt: false,
|
enableJlpt: false,
|
||||||
preserveLineBreaks: false,
|
preserveLineBreaks: false,
|
||||||
@@ -57,4 +57,21 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle'> = {
|
|||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
subtitleSidebar: {
|
||||||
|
enabled: false,
|
||||||
|
layout: 'overlay',
|
||||||
|
toggleKey: 'Backslash',
|
||||||
|
pauseVideoOnHover: false,
|
||||||
|
autoScroll: true,
|
||||||
|
maxWidth: 420,
|
||||||
|
opacity: 0.95,
|
||||||
|
backgroundColor: 'rgba(73, 77, 100, 0.9)',
|
||||||
|
textColor: '#cad3f5',
|
||||||
|
fontFamily: '"M PLUS 1", "Noto Sans CJK JP", sans-serif',
|
||||||
|
fontSize: 16,
|
||||||
|
timestampColor: '#a5adcb',
|
||||||
|
activeLineColor: '#f5bde6',
|
||||||
|
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||||
|
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,5 +110,95 @@ export function buildSubtitleConfigOptionRegistry(
|
|||||||
description:
|
description:
|
||||||
'Five colors used for rank bands when mode is `banded` (from most common to least within topX).',
|
'Five colors used for rank bands when mode is `banded` (from most common to least within topX).',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.enabled',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.enabled,
|
||||||
|
description: 'Enable the subtitle sidebar feature for parsed subtitle sources.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.layout',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.layout,
|
||||||
|
description: 'Render the subtitle sidebar as a floating overlay or reserve space inside mpv.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.toggleKey',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.toggleKey,
|
||||||
|
description: 'KeyboardEvent.code used to toggle the subtitle sidebar open and closed.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.pauseVideoOnHover',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.pauseVideoOnHover,
|
||||||
|
description: 'Pause mpv while hovering the subtitle sidebar, then resume on leave.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.autoScroll',
|
||||||
|
kind: 'boolean',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.autoScroll,
|
||||||
|
description: 'Auto-scroll the active subtitle cue into view while playback advances.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.maxWidth',
|
||||||
|
kind: 'number',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.maxWidth,
|
||||||
|
description: 'Maximum sidebar width in CSS pixels.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.opacity',
|
||||||
|
kind: 'number',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.opacity,
|
||||||
|
description: 'Base opacity applied to the sidebar shell.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.backgroundColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.backgroundColor,
|
||||||
|
description: 'Background color for the subtitle sidebar shell.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.textColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.textColor,
|
||||||
|
description: 'Default cue text color in the subtitle sidebar.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.fontFamily',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.fontFamily,
|
||||||
|
description: 'Font family used for subtitle sidebar cue text.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.fontSize',
|
||||||
|
kind: 'number',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.fontSize,
|
||||||
|
description: 'Base font size for subtitle sidebar cue text in CSS pixels.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.timestampColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.timestampColor,
|
||||||
|
description: 'Timestamp color in the subtitle sidebar.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.activeLineColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.activeLineColor,
|
||||||
|
description: 'Text color for the active subtitle cue.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.activeLineBackgroundColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.activeLineBackgroundColor,
|
||||||
|
description: 'Background color for the active subtitle cue.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'subtitleSidebar.hoverLineBackgroundColor',
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: defaultConfig.subtitleSidebar.hoverLineBackgroundColor,
|
||||||
|
description: 'Background color for hovered subtitle cues.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ const SUBTITLE_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
|||||||
notes: ['Hot-reload: subtitle style changes apply live without restarting SubMiner.'],
|
notes: ['Hot-reload: subtitle style changes apply live without restarting SubMiner.'],
|
||||||
key: 'subtitleStyle',
|
key: 'subtitleStyle',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Subtitle Sidebar',
|
||||||
|
description: ['Parsed-subtitle sidebar cue list styling, behavior, and toggle key.'],
|
||||||
|
notes: ['Hot-reload: subtitle sidebar changes apply live without restarting SubMiner.'],
|
||||||
|
key: 'subtitleSidebar',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
const INTEGRATION_TEMPLATE_SECTIONS: ConfigTemplateSection[] = [
|
||||||
|
|||||||
@@ -418,4 +418,167 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isObject(src.subtitleSidebar)) {
|
||||||
|
const fallback = { ...resolved.subtitleSidebar };
|
||||||
|
resolved.subtitleSidebar = {
|
||||||
|
...resolved.subtitleSidebar,
|
||||||
|
...(src.subtitleSidebar as ResolvedConfig['subtitleSidebar']),
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabled = asBoolean((src.subtitleSidebar as { enabled?: unknown }).enabled);
|
||||||
|
if (enabled !== undefined) {
|
||||||
|
resolved.subtitleSidebar.enabled = enabled;
|
||||||
|
} else if ((src.subtitleSidebar as { enabled?: unknown }).enabled !== undefined) {
|
||||||
|
resolved.subtitleSidebar.enabled = fallback.enabled;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.enabled',
|
||||||
|
(src.subtitleSidebar as { enabled?: unknown }).enabled,
|
||||||
|
resolved.subtitleSidebar.enabled,
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = asString((src.subtitleSidebar as { layout?: unknown }).layout);
|
||||||
|
if (layout === 'overlay' || layout === 'embedded') {
|
||||||
|
resolved.subtitleSidebar.layout = layout;
|
||||||
|
} else if ((src.subtitleSidebar as { layout?: unknown }).layout !== undefined) {
|
||||||
|
resolved.subtitleSidebar.layout = fallback.layout;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.layout',
|
||||||
|
(src.subtitleSidebar as { layout?: unknown }).layout,
|
||||||
|
resolved.subtitleSidebar.layout,
|
||||||
|
'Expected "overlay" or "embedded".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseVideoOnHover = asBoolean(
|
||||||
|
(src.subtitleSidebar as { pauseVideoOnHover?: unknown }).pauseVideoOnHover,
|
||||||
|
);
|
||||||
|
if (pauseVideoOnHover !== undefined) {
|
||||||
|
resolved.subtitleSidebar.pauseVideoOnHover = pauseVideoOnHover;
|
||||||
|
} else if ((src.subtitleSidebar as { pauseVideoOnHover?: unknown }).pauseVideoOnHover !== undefined) {
|
||||||
|
resolved.subtitleSidebar.pauseVideoOnHover = fallback.pauseVideoOnHover;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.pauseVideoOnHover',
|
||||||
|
(src.subtitleSidebar as { pauseVideoOnHover?: unknown }).pauseVideoOnHover,
|
||||||
|
resolved.subtitleSidebar.pauseVideoOnHover,
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoScroll = asBoolean((src.subtitleSidebar as { autoScroll?: unknown }).autoScroll);
|
||||||
|
if (autoScroll !== undefined) {
|
||||||
|
resolved.subtitleSidebar.autoScroll = autoScroll;
|
||||||
|
} else if ((src.subtitleSidebar as { autoScroll?: unknown }).autoScroll !== undefined) {
|
||||||
|
resolved.subtitleSidebar.autoScroll = fallback.autoScroll;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.autoScroll',
|
||||||
|
(src.subtitleSidebar as { autoScroll?: unknown }).autoScroll,
|
||||||
|
resolved.subtitleSidebar.autoScroll,
|
||||||
|
'Expected boolean.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleKey = asString((src.subtitleSidebar as { toggleKey?: unknown }).toggleKey);
|
||||||
|
if (toggleKey !== undefined) {
|
||||||
|
resolved.subtitleSidebar.toggleKey = toggleKey;
|
||||||
|
} else if ((src.subtitleSidebar as { toggleKey?: unknown }).toggleKey !== undefined) {
|
||||||
|
resolved.subtitleSidebar.toggleKey = fallback.toggleKey;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.toggleKey',
|
||||||
|
(src.subtitleSidebar as { toggleKey?: unknown }).toggleKey,
|
||||||
|
resolved.subtitleSidebar.toggleKey,
|
||||||
|
'Expected string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = asNumber((src.subtitleSidebar as { maxWidth?: unknown }).maxWidth);
|
||||||
|
if (maxWidth !== undefined && maxWidth > 0) {
|
||||||
|
resolved.subtitleSidebar.maxWidth = Math.floor(maxWidth);
|
||||||
|
} else if ((src.subtitleSidebar as { maxWidth?: unknown }).maxWidth !== undefined) {
|
||||||
|
resolved.subtitleSidebar.maxWidth = fallback.maxWidth;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.maxWidth',
|
||||||
|
(src.subtitleSidebar as { maxWidth?: unknown }).maxWidth,
|
||||||
|
resolved.subtitleSidebar.maxWidth,
|
||||||
|
'Expected positive number.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const opacity = asNumber((src.subtitleSidebar as { opacity?: unknown }).opacity);
|
||||||
|
if (opacity !== undefined && opacity > 0 && opacity <= 1) {
|
||||||
|
resolved.subtitleSidebar.opacity = opacity;
|
||||||
|
} else if ((src.subtitleSidebar as { opacity?: unknown }).opacity !== undefined) {
|
||||||
|
resolved.subtitleSidebar.opacity = fallback.opacity;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.opacity',
|
||||||
|
(src.subtitleSidebar as { opacity?: unknown }).opacity,
|
||||||
|
resolved.subtitleSidebar.opacity,
|
||||||
|
'Expected number between 0 and 1.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hexColorFields = ['textColor', 'timestampColor', 'activeLineColor'] as const;
|
||||||
|
for (const field of hexColorFields) {
|
||||||
|
const value = asColor((src.subtitleSidebar as Record<string, unknown>)[field]);
|
||||||
|
if (value !== undefined) {
|
||||||
|
resolved.subtitleSidebar[field] = value;
|
||||||
|
} else if ((src.subtitleSidebar as Record<string, unknown>)[field] !== undefined) {
|
||||||
|
resolved.subtitleSidebar[field] = fallback[field];
|
||||||
|
warn(
|
||||||
|
`subtitleSidebar.${field}`,
|
||||||
|
(src.subtitleSidebar as Record<string, unknown>)[field],
|
||||||
|
resolved.subtitleSidebar[field],
|
||||||
|
'Expected hex color.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssColorFields = [
|
||||||
|
'backgroundColor',
|
||||||
|
'activeLineBackgroundColor',
|
||||||
|
'hoverLineBackgroundColor',
|
||||||
|
] as const;
|
||||||
|
for (const field of cssColorFields) {
|
||||||
|
const value = asString((src.subtitleSidebar as Record<string, unknown>)[field]);
|
||||||
|
if (value !== undefined && value.trim().length > 0) {
|
||||||
|
resolved.subtitleSidebar[field] = value.trim();
|
||||||
|
} else if ((src.subtitleSidebar as Record<string, unknown>)[field] !== undefined) {
|
||||||
|
resolved.subtitleSidebar[field] = fallback[field];
|
||||||
|
warn(
|
||||||
|
`subtitleSidebar.${field}`,
|
||||||
|
(src.subtitleSidebar as Record<string, unknown>)[field],
|
||||||
|
resolved.subtitleSidebar[field],
|
||||||
|
'Expected string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontFamily = asString((src.subtitleSidebar as { fontFamily?: unknown }).fontFamily);
|
||||||
|
if (fontFamily !== undefined && fontFamily.trim().length > 0) {
|
||||||
|
resolved.subtitleSidebar.fontFamily = fontFamily.trim();
|
||||||
|
} else if ((src.subtitleSidebar as { fontFamily?: unknown }).fontFamily !== undefined) {
|
||||||
|
resolved.subtitleSidebar.fontFamily = fallback.fontFamily;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.fontFamily',
|
||||||
|
(src.subtitleSidebar as { fontFamily?: unknown }).fontFamily,
|
||||||
|
resolved.subtitleSidebar.fontFamily,
|
||||||
|
'Expected non-empty string.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSize = asNumber((src.subtitleSidebar as { fontSize?: unknown }).fontSize);
|
||||||
|
if (fontSize !== undefined && fontSize > 0) {
|
||||||
|
resolved.subtitleSidebar.fontSize = fontSize;
|
||||||
|
} else if ((src.subtitleSidebar as { fontSize?: unknown }).fontSize !== undefined) {
|
||||||
|
resolved.subtitleSidebar.fontSize = fallback.fontSize;
|
||||||
|
warn(
|
||||||
|
'subtitleSidebar.fontSize',
|
||||||
|
(src.subtitleSidebar as { fontSize?: unknown }).fontSize,
|
||||||
|
resolved.subtitleSidebar.fontSize,
|
||||||
|
'Expected positive number.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
src/config/resolve/subtitle-sidebar.test.ts
Normal file
66
src/config/resolve/subtitle-sidebar.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createResolveContext } from './context';
|
||||||
|
import { applySubtitleDomainConfig } from './subtitle-domains';
|
||||||
|
|
||||||
|
test('subtitleSidebar resolves valid values and preserves dedicated defaults', () => {
|
||||||
|
const { context } = createResolveContext({
|
||||||
|
subtitleSidebar: {
|
||||||
|
enabled: true,
|
||||||
|
layout: 'embedded',
|
||||||
|
toggleKey: 'KeyB',
|
||||||
|
pauseVideoOnHover: true,
|
||||||
|
autoScroll: false,
|
||||||
|
maxWidth: 540,
|
||||||
|
opacity: 0.72,
|
||||||
|
backgroundColor: 'rgba(36, 39, 58, 0.72)',
|
||||||
|
textColor: '#cad3f5',
|
||||||
|
fontFamily: '"Iosevka Aile", sans-serif',
|
||||||
|
fontSize: 17,
|
||||||
|
timestampColor: '#a5adcb',
|
||||||
|
activeLineColor: '#f5bde6',
|
||||||
|
activeLineBackgroundColor: 'rgba(138, 173, 244, 0.22)',
|
||||||
|
hoverLineBackgroundColor: 'rgba(54, 58, 79, 0.84)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applySubtitleDomainConfig(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.enabled, true);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.layout, 'embedded');
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.toggleKey, 'KeyB');
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.pauseVideoOnHover, true);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.autoScroll, false);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.maxWidth, 540);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.opacity, 0.72);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.fontFamily, '"Iosevka Aile", sans-serif');
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.fontSize, 17);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subtitleSidebar falls back and warns on invalid values', () => {
|
||||||
|
const { context, warnings } = createResolveContext({
|
||||||
|
subtitleSidebar: {
|
||||||
|
enabled: 'yes' as never,
|
||||||
|
layout: 'floating' as never,
|
||||||
|
maxWidth: -1,
|
||||||
|
opacity: 5,
|
||||||
|
fontSize: 0,
|
||||||
|
textColor: 'blue',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
applySubtitleDomainConfig(context);
|
||||||
|
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.enabled, false);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.layout, 'overlay');
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.maxWidth, 420);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.opacity, 0.95);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.fontSize, 16);
|
||||||
|
assert.equal(context.resolved.subtitleSidebar.textColor, '#cad3f5');
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.enabled'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.layout'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.maxWidth'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.opacity'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.fontSize'));
|
||||||
|
assert.ok(warnings.some((warning) => warning.path === 'subtitleSidebar.textColor'));
|
||||||
|
});
|
||||||
@@ -42,6 +42,9 @@ function classifyDiff(prev: ResolvedConfig, next: ResolvedConfig): ConfigHotRelo
|
|||||||
if (!isEqual(prev.shortcuts, next.shortcuts)) {
|
if (!isEqual(prev.shortcuts, next.shortcuts)) {
|
||||||
hotReloadFields.push('shortcuts');
|
hotReloadFields.push('shortcuts');
|
||||||
}
|
}
|
||||||
|
if (!isEqual(prev.subtitleSidebar, next.subtitleSidebar)) {
|
||||||
|
hotReloadFields.push('subtitleSidebar');
|
||||||
|
}
|
||||||
if (prev.secondarySub.defaultMode !== next.secondarySub.defaultMode) {
|
if (prev.secondarySub.defaultMode !== next.secondarySub.defaultMode) {
|
||||||
hotReloadFields.push('secondarySub.defaultMode');
|
hotReloadFields.push('secondarySub.defaultMode');
|
||||||
}
|
}
|
||||||
@@ -55,7 +58,7 @@ function classifyDiff(prev: ResolvedConfig, next: ResolvedConfig): ConfigHotRelo
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (key === 'subtitleStyle' || key === 'keybindings' || key === 'shortcuts') {
|
if (key === 'subtitleStyle' || key === 'keybindings' || key === 'shortcuts' || key === 'subtitleSidebar') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user