feat(subtitles): add regex subtitle filters with built-in presets (#318)

- Add `subtitleFilter` config with four presets: parentheses, brackets, speaker prefix and music symbols. Custom regex rules run after the presets.
- Filter primary text where it enters the app (live `sub-text` and parsed cues), so the overlay, sidebar, mining and stats all get the same filtered text.
- Changes hot-reload: the current line is read again and the active track is parsed again, using the filter config as the prefetch parse key.
- Add a custom rule editor in Settings under Behavior → Subtitle Filters that checks patterns as you type.
- Update the config example, the configuration docs and the architecture domains doc, and add a changelog fragment.
This commit is contained in:
sudacode authored and GitHub committed 2026-11-03 00:33:58 -08:00
1 parent 848a6c4021
commit fa3051f9cb
28 files changed
+740 -11

No files matched your search

+18
View File
@@ -6,6 +6,10 @@ import type {
ConfigSettingsField,
} from '../../types/settings';
import { CONFIG_OPTION_REGISTRY, DEFAULT_CONFIG } from '../definitions';
import {
SUBTITLE_FILTER_PRESET_IDS,
SUBTITLE_FILTER_PRESETS,
} from '../../shared/subtitle-text-filter';
import {
getSubtitleCssManagedConfigPaths,
getSubtitleCssScopeForPath,
@@ -127,6 +131,7 @@ const SECTION_ORDER = new Map<string, number>(
'Subtitle Sidebar Appearance',
'Playback Behavior',
'Subtitle Behavior',
'Subtitle Filters',
'Subtitle Sidebar Behavior',
'YouTube Playback Settings',
'mpv Playback',
@@ -203,6 +208,7 @@ const PATH_ORDER = new Map<string, number>(
'logging.files.app',
'logging.files.launcher',
'logging.files.mpv',
...SUBTITLE_FILTER_PRESET_IDS.map((id) => `subtitleFilter.presets.${id}`),
].map((path, index) => [path, index]),
);
@@ -253,6 +259,12 @@ const LABEL_OVERRIDES: Record<string, string> = {
'ankiConnect.media.mirrorMpvVolume': 'Mirror mpv Volume',
'ankiConnect.media.reviewTiming': 'Review Media Timing',
'discordPresence.updateIntervalMs': 'Update Interval (ms)',
...Object.fromEntries(
SUBTITLE_FILTER_PRESET_IDS.map((id) => [
`subtitleFilter.presets.${id}`,
SUBTITLE_FILTER_PRESETS[id].label,
]),
),
};
const DESCRIPTION_OVERRIDES: Record<string, string> = {
@@ -280,6 +292,8 @@ const DESCRIPTION_OVERRIDES: Record<string, string> = {
'Built-in subtitle WebSocket server mode. Auto starts the built-in server only when mpv_websocket is not detected; otherwise it defers to the plugin.',
'discordPresence.updateIntervalMs':
'Minimum interval between presence payload updates, in milliseconds.',
'subtitleFilter.customRules':
'Your own regex rules, applied in order after the presets. ^ and $ match each line. Leave the replacement empty to remove the match.',
};
function isRecord(value: unknown): value is Record<string, unknown> {
@@ -353,6 +367,9 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
if (path === 'subtitleStyle.preserveLineBreaks') {
return { category: 'behavior', section: 'Subtitle Behavior' };
}
if (path.startsWith('subtitleFilter.')) {
return { category: 'behavior', section: 'Subtitle Filters' };
}
if (
path === 'ankiConnect.knownWords.addMinedWordsImmediately' ||
path === 'ankiConnect.knownWords.decks' ||
@@ -557,6 +574,7 @@ function controlForPath(path: string, value: unknown): ConfigSettingsControl {
if (path.startsWith('subtitleStyle.jlptColors.')) return 'color';
if (path.startsWith('subtitleStyle.knownWordMaturityColors.')) return 'color';
if (path === 'subtitleStyle.frequencyDictionary.bandedColors') return 'color-list';
if (path === 'subtitleFilter.customRules') return 'subtitle-filter-rules';
if (OPTION_BY_PATH.get(path)?.enumValues?.length) return 'select';
if (JSON_OBJECT_FIELDS.has(path)) return 'json';
if (Array.isArray(value)) return 'string-list';