mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 15:13:32 -07:00
feat: add Anki deck dropdown with Yomitan auto-fill in settings (#95)
This commit is contained in:
@@ -49,3 +49,10 @@ test('known word deck rename selection keeps current deck on collision', () => {
|
||||
'Core',
|
||||
);
|
||||
});
|
||||
|
||||
test('Anki deck autofill uses inferred Yomitan deck only for untouched empty values', () => {
|
||||
assert.equal(ankiControls.chooseAnkiDeckAutofillValue('', 'Mining', false), 'Mining');
|
||||
assert.equal(ankiControls.chooseAnkiDeckAutofillValue('Current', 'Mining', false), null);
|
||||
assert.equal(ankiControls.chooseAnkiDeckAutofillValue('', 'Mining', true), null);
|
||||
assert.equal(ankiControls.chooseAnkiDeckAutofillValue('', ' ', false), null);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,11 @@ const state: {
|
||||
modelFieldNames: Map<string, string[]>;
|
||||
modelFieldNamesLoading: Set<string>;
|
||||
modelFieldNamesErrors: Map<string, string>;
|
||||
yomitanAnkiDeckName: string | null;
|
||||
yomitanAnkiDeckNameLoading: boolean;
|
||||
yomitanAnkiDeckNameError: string | null;
|
||||
ankiDeckNameManuallySelected: boolean;
|
||||
ankiDeckNameAutofilled: boolean;
|
||||
noteFieldModelName: string;
|
||||
ankiConnectUrl: string;
|
||||
noteFieldModelNameManuallySelected: boolean;
|
||||
@@ -35,6 +40,11 @@ const state: {
|
||||
modelFieldNames: new Map(),
|
||||
modelFieldNamesLoading: new Set(),
|
||||
modelFieldNamesErrors: new Map(),
|
||||
yomitanAnkiDeckName: null,
|
||||
yomitanAnkiDeckNameLoading: false,
|
||||
yomitanAnkiDeckNameError: null,
|
||||
ankiDeckNameManuallySelected: false,
|
||||
ankiDeckNameAutofilled: false,
|
||||
noteFieldModelName: '',
|
||||
ankiConnectUrl: '',
|
||||
noteFieldModelNameManuallySelected: false,
|
||||
@@ -49,6 +59,11 @@ export function configureAnkiControls(options: { requestRender: () => void }): v
|
||||
export function initializeAnkiControls(_values: Record<string, ConfigSettingsSnapshotValue>): void {
|
||||
state.noteFieldModelName = '';
|
||||
state.noteFieldModelNameManuallySelected = false;
|
||||
state.yomitanAnkiDeckName = null;
|
||||
state.yomitanAnkiDeckNameLoading = false;
|
||||
state.yomitanAnkiDeckNameError = null;
|
||||
state.ankiDeckNameManuallySelected = false;
|
||||
state.ankiDeckNameAutofilled = false;
|
||||
}
|
||||
|
||||
export function selectPreferredNoteFieldModelName(
|
||||
@@ -90,6 +105,16 @@ export function chooseKnownWordsDeckRenameValue(
|
||||
return nextDeckName;
|
||||
}
|
||||
|
||||
export function chooseAnkiDeckAutofillValue(
|
||||
currentDeckName: string,
|
||||
inferredDeckName: string,
|
||||
manuallySelected: boolean,
|
||||
): string | null {
|
||||
const current = currentDeckName.trim();
|
||||
const inferred = inferredDeckName.trim();
|
||||
return !manuallySelected && current.length === 0 && inferred.length > 0 ? inferred : null;
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value)
|
||||
? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)
|
||||
@@ -195,6 +220,28 @@ async function loadAnkiDeckNames(draftUrl?: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadYomitanAnkiDeckName(): Promise<void> {
|
||||
if (state.yomitanAnkiDeckName !== null || state.yomitanAnkiDeckNameLoading) return;
|
||||
state.yomitanAnkiDeckNameLoading = true;
|
||||
try {
|
||||
const result = await window.configSettingsAPI.getYomitanAnkiDeckName();
|
||||
if (result.ok) {
|
||||
state.yomitanAnkiDeckName = result.value.trim();
|
||||
state.yomitanAnkiDeckNameError = null;
|
||||
} else {
|
||||
state.yomitanAnkiDeckName = '';
|
||||
state.yomitanAnkiDeckNameError = result.error ?? 'Failed to read Yomitan Anki deck.';
|
||||
}
|
||||
} catch (error) {
|
||||
state.yomitanAnkiDeckName = '';
|
||||
state.yomitanAnkiDeckNameError =
|
||||
error instanceof Error ? error.message : 'Failed to read Yomitan Anki deck.';
|
||||
} finally {
|
||||
state.yomitanAnkiDeckNameLoading = false;
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnkiDeckFieldNames(deckName: string, draftUrl?: string): Promise<void> {
|
||||
syncAnkiConnectUrl(draftUrl);
|
||||
if (
|
||||
@@ -409,6 +456,54 @@ export function renderAnkiNoteTypeInput(
|
||||
return wrap;
|
||||
}
|
||||
|
||||
export function renderAnkiDeckInput(
|
||||
context: SettingsControlContext,
|
||||
field: ConfigSettingsField,
|
||||
): HTMLElement {
|
||||
const draftUrl = getDraftAnkiConnectUrl(context);
|
||||
void loadAnkiDeckNames(draftUrl);
|
||||
void loadYomitanAnkiDeckName();
|
||||
|
||||
const currentValue = context.valueForField(field);
|
||||
let current = typeof currentValue === 'string' ? currentValue.trim() : '';
|
||||
const inferred = state.yomitanAnkiDeckName ?? '';
|
||||
const autofillValue =
|
||||
state.ankiDeckNameAutofilled === false
|
||||
? chooseAnkiDeckAutofillValue(current, inferred, state.ankiDeckNameManuallySelected)
|
||||
: null;
|
||||
if (autofillValue !== null) {
|
||||
state.ankiDeckNameAutofilled = true;
|
||||
current = autofillValue;
|
||||
context.updateDraft(field.configPath, autofillValue);
|
||||
}
|
||||
|
||||
const select = createElement('select', 'config-input') as HTMLSelectElement;
|
||||
addOption(select, '', state.deckNamesLoading ? 'Loading Decks...' : 'Select Deck');
|
||||
for (const deckName of uniqueSorted([...(state.deckNames ?? []), current])) {
|
||||
if (!deckName) continue;
|
||||
addOption(select, deckName);
|
||||
}
|
||||
select.value = current;
|
||||
select.addEventListener('change', () => {
|
||||
state.ankiDeckNameManuallySelected = true;
|
||||
state.ankiDeckNameAutofilled = true;
|
||||
context.updateDraft(field.configPath, select.value);
|
||||
});
|
||||
|
||||
const wrap = createElement('div', 'stacked-control');
|
||||
wrap.append(select);
|
||||
if (state.deckNamesError) {
|
||||
const hint = createElement('div', 'control-hint error');
|
||||
hint.textContent = state.deckNamesError;
|
||||
wrap.append(hint);
|
||||
} else if (state.yomitanAnkiDeckNameError && !state.yomitanAnkiDeckNameLoading) {
|
||||
const hint = createElement('div', 'control-hint');
|
||||
hint.textContent = state.yomitanAnkiDeckNameError;
|
||||
wrap.append(hint);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
export function renderAnkiFieldInput(
|
||||
context: SettingsControlContext,
|
||||
field: ConfigSettingsField,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toConfigDraftValue, toSettingsDisplayValue } from './settings-model';
|
||||
import { parseOptionalNumberInputValue } from './input-values';
|
||||
import {
|
||||
configureAnkiControls,
|
||||
renderAnkiDeckInput,
|
||||
initializeAnkiControls,
|
||||
renderAnkiFieldInput,
|
||||
renderAnkiNoteTypeInput,
|
||||
@@ -162,6 +163,10 @@ export function renderControl(
|
||||
return renderKnownWordsDecksInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'anki-deck') {
|
||||
return renderAnkiDeckInput(context, field);
|
||||
}
|
||||
|
||||
if (field.control === 'anki-note-type') {
|
||||
return renderAnkiNoteTypeInput(context, field);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user