mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-27 00:55:16 -07:00
feat(config): reorganize settings window and move annotation colors to subtitleStyle
- Reorganize Configuration window into Appearance, Behavior, Anki, Input, and Integration sections - Add AnkiConnect-backed deck, note-type, and field pickers in the Anki section - Add click-to-learn keybinding controls - Move known-word and N+1 highlight colors to subtitleStyle.knownWordColor / subtitleStyle.nPlusOneColor; legacy ankiConnect.knownWords.color and ankiConnect.nPlusOne.nPlusOne keys still accepted with deprecation warnings - Add deckNames, modelNames, modelFieldNames, and fieldNamesForDeck methods to AnkiConnectClient - Mark discordPresence.presenceStyle as an enum in the config registry
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import type { ConfigSettingsField, ConfigSettingsSnapshotValue } from '../types/settings';
|
||||
import { parseOptionalNumberInputValue } from './input-values';
|
||||
import {
|
||||
configureAnkiControls,
|
||||
initializeAnkiControls,
|
||||
renderAnkiFieldInput,
|
||||
renderAnkiNoteTypeInput,
|
||||
renderKnownWordsDecksInput,
|
||||
renderNoteFieldModelPicker,
|
||||
} from './settings-anki-controls';
|
||||
import type { SettingsControlContext } from './settings-control-context';
|
||||
import { createElement, isSecretSnapshotValue } from './settings-control-dom';
|
||||
import { renderKeyboardInput, renderMpvKeybindingsInput } from './settings-keybinding-controls';
|
||||
|
||||
export { renderNoteFieldModelPicker };
|
||||
|
||||
export function configureSettingsControls(options: { requestRender: () => void }): void {
|
||||
configureAnkiControls(options);
|
||||
}
|
||||
|
||||
export function initializeSettingsControls(
|
||||
values: Record<string, ConfigSettingsSnapshotValue>,
|
||||
): void {
|
||||
initializeAnkiControls(values);
|
||||
}
|
||||
|
||||
function renderColorListInput(
|
||||
context: SettingsControlContext,
|
||||
field: ConfigSettingsField,
|
||||
value: ConfigSettingsSnapshotValue,
|
||||
): HTMLElement {
|
||||
const colors = Array.isArray(value) ? (value as string[]) : [];
|
||||
const container = createElement('div', 'color-list');
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
const row = createElement('div', 'color-list-row');
|
||||
const label = createElement('span', 'color-list-label');
|
||||
label.textContent = `Band ${i + 1}`;
|
||||
const input = createElement('input', 'config-input') as HTMLInputElement;
|
||||
input.type = 'color';
|
||||
input.value = colors[i] ?? '#000000';
|
||||
input.addEventListener('input', () => {
|
||||
const updated = [...colors];
|
||||
updated[i] = input.value;
|
||||
context.updateDraft(field.configPath, updated);
|
||||
});
|
||||
row.append(label, input);
|
||||
container.append(row);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
function renderJsonInput(
|
||||
context: SettingsControlContext,
|
||||
field: ConfigSettingsField,
|
||||
value: ConfigSettingsSnapshotValue,
|
||||
): HTMLElement {
|
||||
const textarea = createElement('textarea', 'config-textarea') as HTMLTextAreaElement;
|
||||
textarea.spellcheck = false;
|
||||
textarea.value = JSON.stringify(value ?? {}, null, 2);
|
||||
textarea.addEventListener('input', () => {
|
||||
try {
|
||||
context.updateDraft(field.configPath, JSON.parse(textarea.value));
|
||||
textarea.classList.remove('invalid');
|
||||
context.setFieldError(field.configPath, null);
|
||||
} catch {
|
||||
textarea.classList.add('invalid');
|
||||
context.setFieldError(field.configPath, 'Invalid JSON');
|
||||
}
|
||||
});
|
||||
return textarea;
|
||||
}
|
||||
|
||||
function renderStringListInput(
|
||||
context: SettingsControlContext,
|
||||
field: ConfigSettingsField,
|
||||
value: ConfigSettingsSnapshotValue,
|
||||
): HTMLElement {
|
||||
const textarea = createElement('textarea', 'config-textarea compact') as HTMLTextAreaElement;
|
||||
textarea.spellcheck = false;
|
||||
textarea.value = Array.isArray(value) ? value.join('\n') : '';
|
||||
textarea.addEventListener('input', () => {
|
||||
context.updateDraft(
|
||||
field.configPath,
|
||||
textarea.value
|
||||
.split('\n')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
});
|
||||
return textarea;
|
||||
}
|
||||
|
||||
export function renderControl(
|
||||
field: ConfigSettingsField,
|
||||
context: SettingsControlContext,
|
||||
): HTMLElement {
|
||||
const value = context.valueForField(field);
|
||||
|
||||
if (field.control === 'keyboard-shortcut') {
|
||||
return renderKeyboardInput(context, field, 'accelerator');
|
||||
}
|
||||
|
||||
if (field.control === 'key-code') {
|
||||
return renderKeyboardInput(context, field, 'code');
|
||||
}
|
||||
|
||||
if (field.control === 'known-words-decks') {
|
||||
return renderKnownWordsDecksInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'anki-note-type') {
|
||||
return renderAnkiNoteTypeInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'anki-field') {
|
||||
return renderAnkiFieldInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'mpv-keybindings') {
|
||||
return renderMpvKeybindingsInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'boolean') {
|
||||
const label = createElement('label', 'switch-control');
|
||||
const input = createElement('input') as HTMLInputElement;
|
||||
input.type = 'checkbox';
|
||||
input.checked = Boolean(value);
|
||||
input.addEventListener('change', () => context.updateDraft(field.configPath, input.checked));
|
||||
const track = createElement('span', 'switch-track');
|
||||
label.append(input, track);
|
||||
return label;
|
||||
}
|
||||
|
||||
if (field.control === 'number') {
|
||||
const input = createElement('input', 'config-input') as HTMLInputElement;
|
||||
input.type = 'number';
|
||||
input.value = typeof value === 'number' ? String(value) : '';
|
||||
input.addEventListener('input', () => {
|
||||
const next = parseOptionalNumberInputValue(input.value);
|
||||
if (next.ok) {
|
||||
input.classList.remove('invalid');
|
||||
context.setFieldError(field.configPath, null);
|
||||
context.updateDraft(field.configPath, next.value);
|
||||
} else {
|
||||
input.classList.add('invalid');
|
||||
context.setFieldError(field.configPath, 'Invalid number');
|
||||
}
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
||||
if (field.control === 'select') {
|
||||
const select = createElement('select', 'config-input') as HTMLSelectElement;
|
||||
for (const enumValue of field.enumValues ?? []) {
|
||||
const option = createElement('option') as HTMLOptionElement;
|
||||
option.value = enumValue;
|
||||
option.textContent = enumValue;
|
||||
option.selected = enumValue === value;
|
||||
select.append(option);
|
||||
}
|
||||
select.addEventListener('change', () => context.updateDraft(field.configPath, select.value));
|
||||
return select;
|
||||
}
|
||||
|
||||
if (field.control === 'color-list') {
|
||||
return renderColorListInput(context, field, value);
|
||||
}
|
||||
|
||||
if (field.control === 'string-list') {
|
||||
return renderStringListInput(context, field, value);
|
||||
}
|
||||
|
||||
if (field.control === 'json') {
|
||||
return renderJsonInput(context, field, value);
|
||||
}
|
||||
|
||||
if (field.control === 'textarea') {
|
||||
const textarea = createElement('textarea', 'config-textarea compact') as HTMLTextAreaElement;
|
||||
textarea.spellcheck = false;
|
||||
textarea.value = typeof value === 'string' ? value : '';
|
||||
textarea.addEventListener('input', () => context.updateDraft(field.configPath, textarea.value));
|
||||
return textarea;
|
||||
}
|
||||
|
||||
const input = createElement('input', 'config-input') as HTMLInputElement;
|
||||
input.type = field.control === 'secret' ? 'password' : field.control;
|
||||
if (field.control === 'secret') {
|
||||
input.placeholder =
|
||||
isSecretSnapshotValue(value) && value.configured ? 'Configured' : 'Not configured';
|
||||
input.addEventListener('input', () => {
|
||||
if (input.value.trim().length === 0) {
|
||||
context.updateDraft(field.configPath, value);
|
||||
return;
|
||||
}
|
||||
context.updateDraft(field.configPath, input.value);
|
||||
});
|
||||
} else {
|
||||
input.value = typeof value === 'string' ? value : '';
|
||||
input.addEventListener('input', () => context.updateDraft(field.configPath, input.value));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
Reference in New Issue
Block a user