Files
SubMiner/src/settings/settings-model.ts
T
sudacode 6ba91780c1 feat(config): unify mpv plugin options under main config and add CSS/Ani
- Replace subminer.conf plugin config with mpv.* fields in config.jsonc
- Add socketPath, backend, autoStartSubMiner, pauseUntilOverlayReady, aniskipEnabled/buttonKey, subminerBinaryPath to mpv config
- Add subtitleSidebar.css field; migrate legacy sidebar appearance fields
- Add paintOrder and WebkitTextStroke to subtitle style options
- Update default subtitle/sidebar fontFamily to CJK-first stack
- Fix overlay visible state surviving mpv y-r restart
- Fix live config saves applying subtitle CSS immediately to open overlays
- Migrate legacy primary/secondary subtitle appearance into subtitleStyle.css on load
- Switch AniSkip button key setting to click-to-learn key capture
2026-05-18 03:07:39 -07:00

108 lines
2.8 KiB
TypeScript

import type {
ConfigSettingsCategory,
ConfigSettingsField,
ConfigSettingsPatchOperation,
ConfigSettingsSnapshotValue,
} from '../types/settings';
export interface SettingsFilter {
category?: ConfigSettingsCategory;
query?: string;
}
export interface SettingsDraft {
readonly initialValues: Record<string, ConfigSettingsSnapshotValue>;
readonly values: Record<string, ConfigSettingsSnapshotValue>;
readonly resetPaths: Set<string>;
}
function normalizeQuery(query: string | undefined): string {
return (query ?? '').trim().toLowerCase();
}
function searchableText(parts: Array<string | undefined>): string {
return parts
.filter(Boolean)
.join(' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.toLowerCase();
}
function valuesEqual(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
export function filterSettingsFields(
fields: ConfigSettingsField[],
filter: SettingsFilter,
): ConfigSettingsField[] {
const query = normalizeQuery(filter.query);
const terms = query.length > 0 ? searchableText([query]).split(/\s+/).filter(Boolean) : [];
return fields.filter((field) => {
if (field.legacyHidden || field.settingsHidden) {
return false;
}
if (filter.category && field.category !== filter.category) {
return false;
}
if (!query || terms.length === 0) {
return true;
}
const haystack = searchableText([
field.label,
field.description,
field.configPath,
field.section,
field.subsection ?? '',
field.enumValues?.join(' ') ?? '',
]);
return terms.every((term) => haystack.includes(term));
});
}
export function createSettingsDraft(
values: Record<string, ConfigSettingsSnapshotValue>,
): SettingsDraft {
return {
initialValues: structuredClone(values),
values: structuredClone(values),
resetPaths: new Set(),
};
}
export function setDraftValue(
draft: SettingsDraft,
path: string,
value: ConfigSettingsSnapshotValue,
): void {
draft.values[path] = value;
draft.resetPaths.delete(path);
}
export function resetDraftPath(draft: SettingsDraft, path: string, defaultValue: unknown): void {
draft.values[path] = structuredClone(defaultValue);
draft.resetPaths.add(path);
}
export function getDirtyOperations(draft: SettingsDraft): ConfigSettingsPatchOperation[] {
const operations: ConfigSettingsPatchOperation[] = [];
const paths = new Set([...Object.keys(draft.initialValues), ...Object.keys(draft.values)]);
for (const path of [...paths].sort()) {
if (draft.resetPaths.has(path)) {
operations.push({ op: 'reset', path });
continue;
}
if (!valuesEqual(draft.values[path], draft.initialValues[path])) {
operations.push({
op: 'set',
path,
value: draft.values[path],
});
}
}
return operations;
}