mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
fix: address config modal review feedback
This commit is contained in:
@@ -123,3 +123,36 @@ test('AnkiConnectClient derives field names from sampled notes in a deck', async
|
||||
params: { notes: [3, 1, 2] },
|
||||
});
|
||||
});
|
||||
|
||||
test('AnkiConnectClient derives model names from sampled notes in a deck', async () => {
|
||||
const client = new AnkiConnectClient('http://127.0.0.1:8765') as unknown as {
|
||||
client: { post: (url: string, body: { action: string; params: unknown }) => Promise<unknown> };
|
||||
};
|
||||
const calls: Array<{ action: string; params: unknown }> = [];
|
||||
client.client = {
|
||||
post: async (_url, body) => {
|
||||
calls.push({ action: body.action, params: body.params });
|
||||
if (body.action === 'findNotes') {
|
||||
return { data: { result: [5, 4], error: null } };
|
||||
}
|
||||
if (body.action === 'notesInfo') {
|
||||
return {
|
||||
data: {
|
||||
result: [{ modelName: 'Lapis Morph' }, { modelName: 'Kiku' }],
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { data: { result: [], error: null } };
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(await (client as unknown as AnkiConnectClient).modelNamesForDeck('Mining'), [
|
||||
'Kiku',
|
||||
'Lapis Morph',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.action),
|
||||
['findNotes', 'notesInfo'],
|
||||
);
|
||||
});
|
||||
|
||||
+21
-2
@@ -177,14 +177,21 @@ export class AnkiConnectClient {
|
||||
: [];
|
||||
}
|
||||
|
||||
async fieldNamesForDeck(deckName: string, sampleSize = 100): Promise<string[]> {
|
||||
private async noteInfosForDeck(
|
||||
deckName: string,
|
||||
sampleSize = 100,
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const escapedDeckName = deckName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
const noteIds = await this.findNotes(`deck:"${escapedDeckName}"`, { maxRetries: 0 });
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const noteInfos = await this.notesInfo(noteIds.slice(0, sampleSize));
|
||||
return this.notesInfo(noteIds.slice(0, sampleSize));
|
||||
}
|
||||
|
||||
async fieldNamesForDeck(deckName: string, sampleSize = 100): Promise<string[]> {
|
||||
const noteInfos = await this.noteInfosForDeck(deckName, sampleSize);
|
||||
const fields = new Set<string>();
|
||||
for (const noteInfo of noteInfos) {
|
||||
const noteFields = noteInfo.fields;
|
||||
@@ -198,6 +205,18 @@ export class AnkiConnectClient {
|
||||
return [...fields].sort();
|
||||
}
|
||||
|
||||
async modelNamesForDeck(deckName: string, sampleSize = 100): Promise<string[]> {
|
||||
const noteInfos = await this.noteInfosForDeck(deckName, sampleSize);
|
||||
const modelNames = new Set<string>();
|
||||
for (const noteInfo of noteInfos) {
|
||||
const modelName = noteInfo.modelName;
|
||||
if (typeof modelName === 'string' && modelName.length > 0) {
|
||||
modelNames.add(modelName);
|
||||
}
|
||||
}
|
||||
return [...modelNames].sort();
|
||||
}
|
||||
|
||||
async notesInfo(noteIds: number[]): Promise<Record<string, unknown>[]> {
|
||||
const result = await this.invoke('notesInfo', { notes: noteIds });
|
||||
return (result as Record<string, unknown>[]) || [];
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface ConfigSettingsIpcChannels {
|
||||
openConfigSettingsWindow: string;
|
||||
getConfigSettingsAnkiDeckNames: string;
|
||||
getConfigSettingsAnkiDeckFieldNames: string;
|
||||
getConfigSettingsAnkiDeckModelNames: string;
|
||||
getConfigSettingsAnkiModelNames: string;
|
||||
getConfigSettingsAnkiModelFieldNames: string;
|
||||
}
|
||||
@@ -38,6 +39,7 @@ export interface ConfigSettingsIpcChannels {
|
||||
export interface ConfigSettingsAnkiClient {
|
||||
deckNames(): Promise<string[]>;
|
||||
fieldNamesForDeck(deckName: string): Promise<string[]>;
|
||||
modelNamesForDeck(deckName: string): Promise<string[]>;
|
||||
modelNames(): Promise<string[]>;
|
||||
modelFieldNames(modelName: string): Promise<string[]>;
|
||||
}
|
||||
@@ -211,6 +213,15 @@ export function createConfigSettingsRuntime<TWindow extends ConfigSettingsWindow
|
||||
: invalidAnkiListResult('Deck name is required.');
|
||||
},
|
||||
);
|
||||
deps.ipcMain.handle(
|
||||
deps.ipcChannels.getConfigSettingsAnkiDeckModelNames,
|
||||
(_event, deckName, draftUrl) => {
|
||||
const normalizedDeckName = typeof deckName === 'string' ? deckName.trim() : '';
|
||||
return normalizedDeckName
|
||||
? getAnkiList(draftUrl, (client) => client.modelNamesForDeck(normalizedDeckName))
|
||||
: invalidAnkiListResult('Deck name is required.');
|
||||
},
|
||||
);
|
||||
deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsAnkiModelNames, (_event, draftUrl) =>
|
||||
getAnkiList(draftUrl, (client) => client.modelNames()),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ test('settings preload exposes Anki lookup helpers', () => {
|
||||
for (const method of [
|
||||
'getAnkiDeckNames',
|
||||
'getAnkiDeckFieldNames',
|
||||
'getAnkiDeckModelNames',
|
||||
'getAnkiModelNames',
|
||||
'getAnkiModelFieldNames',
|
||||
]) {
|
||||
|
||||
@@ -14,6 +14,7 @@ const SETTINGS_IPC_CHANNELS = {
|
||||
openWindow: 'config:open-settings-window',
|
||||
getAnkiDeckNames: 'config-settings:anki-deck-names',
|
||||
getAnkiDeckFieldNames: 'config-settings:anki-deck-field-names',
|
||||
getAnkiDeckModelNames: 'config-settings:anki-deck-model-names',
|
||||
getAnkiModelNames: 'config-settings:anki-model-names',
|
||||
getAnkiModelFieldNames: 'config-settings:anki-model-field-names',
|
||||
} as const;
|
||||
@@ -32,6 +33,11 @@ const configSettingsAPI: ConfigSettingsAPI = {
|
||||
draftUrl?: string,
|
||||
): Promise<ConfigSettingsAnkiListResult> =>
|
||||
ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiDeckFieldNames, deckName, draftUrl),
|
||||
getAnkiDeckModelNames: (
|
||||
deckName: string,
|
||||
draftUrl?: string,
|
||||
): Promise<ConfigSettingsAnkiListResult> =>
|
||||
ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiDeckModelNames, deckName, draftUrl),
|
||||
getAnkiModelNames: (draftUrl?: string): Promise<ConfigSettingsAnkiListResult> =>
|
||||
ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiModelNames, draftUrl),
|
||||
getAnkiModelFieldNames: (
|
||||
|
||||
@@ -533,6 +533,28 @@ test('mpv input forwarding installs local key handling when session binding IPC
|
||||
}
|
||||
});
|
||||
|
||||
test('mpv input forwarding retries a transient keyboard config IPC failure', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
let calls = 0;
|
||||
|
||||
try {
|
||||
testGlobals.setGetSessionBindings(async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
throw new Error('transient');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
await handlers.setupMpvInputForwarding();
|
||||
await wait(25);
|
||||
|
||||
assert.equal(calls, 2);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('session help chord resolver follows remapped session bindings', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
|
||||
|
||||
@@ -953,13 +953,31 @@ export function createKeyboardHandlers(
|
||||
syncKeyboardTokenSelection();
|
||||
}
|
||||
|
||||
async function loadMpvInputForwardingConfigWithRetry(): Promise<void> {
|
||||
let lastError: unknown = null;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await loadMpvInputForwardingConfig();
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < 2) {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 10 * (attempt + 1));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function setupMpvInputForwarding(): Promise<void> {
|
||||
installMpvInputForwardingListeners();
|
||||
syncKeyboardTokenSelection();
|
||||
|
||||
let configLoadSettled = false;
|
||||
let configLoadError: unknown = null;
|
||||
const configLoad = loadMpvInputForwardingConfig().then(
|
||||
const configLoad = loadMpvInputForwardingConfigWithRetry().then(
|
||||
() => {
|
||||
configLoadSettled = true;
|
||||
},
|
||||
|
||||
@@ -3,7 +3,11 @@ import test from 'node:test';
|
||||
|
||||
import type { ElectronAPI, SubtitleSidebarSnapshot } from '../../types';
|
||||
import { createRendererState } from '../state.js';
|
||||
import { createSubtitleSidebarModal, findActiveSubtitleCueIndex } from './subtitle-sidebar.js';
|
||||
import {
|
||||
applySidebarCssDeclarations,
|
||||
createSubtitleSidebarModal,
|
||||
findActiveSubtitleCueIndex,
|
||||
} from './subtitle-sidebar.js';
|
||||
|
||||
function createClassList(initialTokens: string[] = []) {
|
||||
const tokens = new Set(initialTokens);
|
||||
@@ -108,6 +112,34 @@ test('findActiveSubtitleCueIndex prefers current subtitle timing over near-futur
|
||||
assert.equal(findActiveSubtitleCueIndex(cues, { text: 'previous', startTime: 231 }, 233, 0), 0);
|
||||
});
|
||||
|
||||
test('applySidebarCssDeclarations clears declarations removed by config reload', () => {
|
||||
const removed: string[] = [];
|
||||
const style = {
|
||||
color: '',
|
||||
backgroundColor: '',
|
||||
setProperty(property: string, value: string) {
|
||||
(this as unknown as Record<string, string>)[property] = value;
|
||||
},
|
||||
removeProperty(property: string) {
|
||||
removed.push(property);
|
||||
delete (this as unknown as Record<string, string>)[property];
|
||||
},
|
||||
};
|
||||
const target = { style } as unknown as HTMLElement;
|
||||
|
||||
applySidebarCssDeclarations(target, {
|
||||
color: '#cad3f5',
|
||||
'background-color': '#181926',
|
||||
});
|
||||
applySidebarCssDeclarations(target, {
|
||||
color: '#ffffff',
|
||||
});
|
||||
|
||||
assert.equal(style.color, '#ffffff');
|
||||
assert.equal(style.backgroundColor, '');
|
||||
assert.deepEqual(removed, ['background-color']);
|
||||
});
|
||||
|
||||
test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => {
|
||||
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
|
||||
const previousWindow = globals.window;
|
||||
|
||||
@@ -8,6 +8,7 @@ const CLICK_SEEK_OFFSET_SEC = 0.08;
|
||||
const SNAPSHOT_POLL_INTERVAL_MS = 80;
|
||||
const EMBEDDED_SIDEBAR_MIN_WIDTH_PX = 240;
|
||||
const EMBEDDED_SIDEBAR_MAX_RATIO = 0.45;
|
||||
const appliedSidebarCssKeys = new WeakMap<HTMLElement, Set<string>>();
|
||||
|
||||
function nowForUiTiming(): number {
|
||||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||||
@@ -55,22 +56,37 @@ function formatCueTimestamp(seconds: number): string {
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function applySidebarCssDeclarations(
|
||||
export function applySidebarCssDeclarations(
|
||||
target: HTMLElement,
|
||||
declarations: Record<string, string>,
|
||||
): void {
|
||||
const targetStyle = (target as HTMLElement & { style?: CSSStyleDeclaration }).style;
|
||||
if (!targetStyle) return;
|
||||
const styleTarget = targetStyle as unknown as Record<string, string>;
|
||||
const previousKeys = appliedSidebarCssKeys.get(target) ?? new Set<string>();
|
||||
const nextKeys = new Set<string>();
|
||||
|
||||
for (const property of previousKeys) {
|
||||
if (Object.prototype.hasOwnProperty.call(declarations, property)) continue;
|
||||
if (property.includes('-')) {
|
||||
targetStyle.removeProperty(property);
|
||||
} else {
|
||||
styleTarget[property] = '';
|
||||
}
|
||||
}
|
||||
|
||||
for (const [property, rawValue] of Object.entries(declarations)) {
|
||||
const value = rawValue.trim();
|
||||
if (value.length === 0) continue;
|
||||
if (property.includes('-')) {
|
||||
targetStyle.setProperty(property, value);
|
||||
continue;
|
||||
} else {
|
||||
styleTarget[property] = value;
|
||||
}
|
||||
const styleTarget = targetStyle as unknown as Record<string, string>;
|
||||
styleTarget[property] = value;
|
||||
nextKeys.add(property);
|
||||
}
|
||||
|
||||
appliedSidebarCssKeys.set(target, nextKeys);
|
||||
}
|
||||
|
||||
export function findActiveSubtitleCueIndex(
|
||||
|
||||
@@ -2,25 +2,22 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as ankiControls from './settings-anki-controls';
|
||||
|
||||
test('note field model preference prefers exact Kiku over configured model', () => {
|
||||
test('note field model preference keeps a matching configured model before Kiku fallback', () => {
|
||||
assert.equal(
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Lapis Morph', 'Kiku'], 'Lapis Morph'),
|
||||
'Kiku',
|
||||
'Lapis Morph',
|
||||
);
|
||||
});
|
||||
|
||||
test('note field model preference ignores configured model case-insensitively', () => {
|
||||
test('note field model preference matches configured model case-insensitively', () => {
|
||||
assert.equal(
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Lapis Morph', 'Kiku'], 'lapis morph'),
|
||||
'Kiku',
|
||||
'Lapis Morph',
|
||||
);
|
||||
});
|
||||
|
||||
test('note field model preference prefers exact Lapis when Kiku is unavailable', () => {
|
||||
assert.equal(
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis'], ''),
|
||||
'Lapis',
|
||||
);
|
||||
assert.equal(ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis'], ''), 'Lapis');
|
||||
});
|
||||
|
||||
test('note field model preference prefers exact Kiku over exact Lapis', () => {
|
||||
@@ -28,16 +25,13 @@ test('note field model preference prefers exact Kiku over exact Lapis', () => {
|
||||
});
|
||||
|
||||
test('note field model preference does not treat partial Kiku matches as Kiku', () => {
|
||||
assert.equal(
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Kikuchi', 'Lapis Morph'], 'Lapis Morph'),
|
||||
'',
|
||||
);
|
||||
assert.equal(ankiControls.selectPreferredNoteFieldModelName(['Kikuchi', 'Mining'], ''), '');
|
||||
});
|
||||
|
||||
test('note field model preference does not treat partial Lapis matches as Lapis', () => {
|
||||
test('note field model preference accepts partial Lapis matches', () => {
|
||||
assert.equal(
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis Morph'], 'Lapis Morph'),
|
||||
'',
|
||||
ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis Morph'], ''),
|
||||
'Lapis Morph',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ const state: {
|
||||
deckFieldNames: Map<string, string[]>;
|
||||
deckFieldNamesLoading: Set<string>;
|
||||
deckFieldNamesErrors: Map<string, string>;
|
||||
deckModelNames: Map<string, string[]>;
|
||||
deckModelNamesLoading: Map<string, Promise<string[]>>;
|
||||
modelNames: string[] | null;
|
||||
modelNamesLoading: boolean;
|
||||
modelNamesError: string | null;
|
||||
@@ -25,6 +27,8 @@ const state: {
|
||||
deckFieldNames: new Map(),
|
||||
deckFieldNamesLoading: new Set(),
|
||||
deckFieldNamesErrors: new Map(),
|
||||
deckModelNames: new Map(),
|
||||
deckModelNamesLoading: new Map(),
|
||||
modelNames: null,
|
||||
modelNamesLoading: false,
|
||||
modelNamesError: null,
|
||||
@@ -55,16 +59,26 @@ export function initializeAnkiControls(values: Record<string, ConfigSettingsSnap
|
||||
|
||||
export function selectPreferredNoteFieldModelName(
|
||||
modelNames: readonly string[],
|
||||
_currentModelName = '',
|
||||
currentModelName = '',
|
||||
): string {
|
||||
const normalizedCurrentModelName = currentModelName.trim().toLowerCase();
|
||||
if (normalizedCurrentModelName) {
|
||||
const currentModel = modelNames.find(
|
||||
(name) => name.toLowerCase() === normalizedCurrentModelName,
|
||||
);
|
||||
if (currentModel) {
|
||||
return currentModel;
|
||||
}
|
||||
}
|
||||
|
||||
const exactKiku = modelNames.find((name) => name.toLowerCase() === 'kiku');
|
||||
if (exactKiku) {
|
||||
return exactKiku;
|
||||
}
|
||||
|
||||
const exactLapis = modelNames.find((name) => name.toLowerCase() === 'lapis');
|
||||
if (exactLapis) {
|
||||
return exactLapis;
|
||||
const lapis = modelNames.find((name) => name.toLowerCase().includes('lapis'));
|
||||
if (lapis) {
|
||||
return lapis;
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -106,6 +120,11 @@ function getDraftAnkiConnectUrl(context: SettingsControlContext): string | undef
|
||||
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function getDraftAnkiDeckName(context: SettingsControlContext): string {
|
||||
const value = context.valueForPath('ankiConnect.deck');
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function syncAnkiConnectUrl(draftUrl: string | undefined): void {
|
||||
const nextUrl = draftUrl ?? '';
|
||||
if (state.ankiConnectUrl === nextUrl) {
|
||||
@@ -116,6 +135,8 @@ function syncAnkiConnectUrl(draftUrl: string | undefined): void {
|
||||
state.deckNamesLoading ||
|
||||
state.deckFieldNames.size > 0 ||
|
||||
state.deckFieldNamesLoading.size > 0 ||
|
||||
state.deckModelNames.size > 0 ||
|
||||
state.deckModelNamesLoading.size > 0 ||
|
||||
state.modelNames !== null ||
|
||||
state.modelNamesLoading ||
|
||||
state.modelFieldNames.size > 0 ||
|
||||
@@ -131,6 +152,8 @@ function syncAnkiConnectUrl(draftUrl: string | undefined): void {
|
||||
state.deckFieldNames.clear();
|
||||
state.deckFieldNamesLoading.clear();
|
||||
state.deckFieldNamesErrors.clear();
|
||||
state.deckModelNames.clear();
|
||||
state.deckModelNamesLoading.clear();
|
||||
state.modelNames = null;
|
||||
state.modelNamesLoading = false;
|
||||
state.modelNamesError = null;
|
||||
@@ -205,9 +228,85 @@ async function loadAnkiDeckFieldNames(deckName: string, draftUrl?: string): Prom
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnkiModelNames(draftUrl?: string): Promise<void> {
|
||||
async function loadAnkiDeckModelNames(deckName: string, draftUrl?: string): Promise<string[]> {
|
||||
syncAnkiConnectUrl(draftUrl);
|
||||
if (state.modelNames || state.modelNamesLoading) return;
|
||||
if (!deckName) return [];
|
||||
const cached = state.deckModelNames.get(deckName);
|
||||
if (cached) return cached;
|
||||
const loading = state.deckModelNamesLoading.get(deckName);
|
||||
if (loading) return loading;
|
||||
|
||||
const requestUrl = state.ankiConnectUrl;
|
||||
const request = window.configSettingsAPI
|
||||
.getAnkiDeckModelNames(deckName, draftUrl)
|
||||
.then((result) => {
|
||||
if (state.ankiConnectUrl !== requestUrl) return [];
|
||||
const values = result.ok ? uniqueSorted(result.values) : [];
|
||||
state.deckModelNames.set(deckName, values);
|
||||
return values;
|
||||
})
|
||||
.catch(() => {
|
||||
if (state.ankiConnectUrl === requestUrl) {
|
||||
state.deckModelNames.set(deckName, []);
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.finally(() => {
|
||||
if (state.ankiConnectUrl === requestUrl) {
|
||||
state.deckModelNamesLoading.delete(deckName);
|
||||
requestRender();
|
||||
}
|
||||
});
|
||||
|
||||
state.deckModelNamesLoading.set(deckName, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function findModelName(modelNames: readonly string[], modelName: string): string {
|
||||
const normalizedModelName = modelName.trim().toLowerCase();
|
||||
return normalizedModelName
|
||||
? (modelNames.find((name) => name.toLowerCase() === normalizedModelName) ?? '')
|
||||
: '';
|
||||
}
|
||||
|
||||
async function updatePreferredNoteFieldModelName(
|
||||
modelNames: readonly string[],
|
||||
deckName: string,
|
||||
draftUrl: string | undefined,
|
||||
requestUrl: string,
|
||||
): Promise<void> {
|
||||
const deckModelNames = await loadAnkiDeckModelNames(deckName, draftUrl);
|
||||
if (state.ankiConnectUrl !== requestUrl || state.noteFieldModelNameManuallySelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nextModelName = '';
|
||||
for (const deckModelName of deckModelNames) {
|
||||
nextModelName = findModelName(modelNames, deckModelName);
|
||||
if (nextModelName) break;
|
||||
}
|
||||
nextModelName ||= selectPreferredNoteFieldModelName(modelNames, state.noteFieldModelName);
|
||||
|
||||
if (state.noteFieldModelName !== nextModelName) {
|
||||
state.noteFieldModelName = nextModelName;
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAnkiModelNames(draftUrl?: string, deckName = ''): Promise<void> {
|
||||
syncAnkiConnectUrl(draftUrl);
|
||||
if (state.modelNames) {
|
||||
if (!state.noteFieldModelNameManuallySelected) {
|
||||
void updatePreferredNoteFieldModelName(
|
||||
state.modelNames,
|
||||
deckName,
|
||||
draftUrl,
|
||||
state.ankiConnectUrl,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state.modelNamesLoading) return;
|
||||
const requestUrl = state.ankiConnectUrl;
|
||||
state.modelNamesLoading = true;
|
||||
try {
|
||||
@@ -217,10 +316,7 @@ async function loadAnkiModelNames(draftUrl?: string): Promise<void> {
|
||||
state.modelNames = uniqueSorted(result.values);
|
||||
state.modelNamesError = null;
|
||||
if (!state.noteFieldModelNameManuallySelected) {
|
||||
state.noteFieldModelName = selectPreferredNoteFieldModelName(
|
||||
state.modelNames,
|
||||
state.noteFieldModelName,
|
||||
);
|
||||
await updatePreferredNoteFieldModelName(state.modelNames, deckName, draftUrl, requestUrl);
|
||||
}
|
||||
} else {
|
||||
state.modelNames = [];
|
||||
@@ -283,7 +379,7 @@ export function renderAnkiNoteTypeInput(
|
||||
field: ConfigSettingsField,
|
||||
): HTMLElement {
|
||||
const draftUrl = getDraftAnkiConnectUrl(context);
|
||||
void loadAnkiModelNames(draftUrl);
|
||||
void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
|
||||
const currentValue = context.valueForField(field);
|
||||
const current = typeof currentValue === 'string' ? currentValue : '';
|
||||
const select = createElement('select', 'config-input') as HTMLSelectElement;
|
||||
@@ -312,7 +408,7 @@ export function renderAnkiFieldInput(
|
||||
field: ConfigSettingsField,
|
||||
): HTMLElement {
|
||||
const draftUrl = getDraftAnkiConnectUrl(context);
|
||||
void loadAnkiModelNames(draftUrl);
|
||||
void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
|
||||
if (state.noteFieldModelName) {
|
||||
void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl);
|
||||
}
|
||||
@@ -350,7 +446,7 @@ export function renderAnkiFieldInput(
|
||||
|
||||
export function renderNoteFieldModelPicker(context: SettingsControlContext): HTMLElement {
|
||||
const draftUrl = getDraftAnkiConnectUrl(context);
|
||||
void loadAnkiModelNames(draftUrl);
|
||||
void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
|
||||
if (state.noteFieldModelName) {
|
||||
void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export const IPC_CHANNELS = {
|
||||
openConfigSettingsWindow: 'config:open-settings-window',
|
||||
getConfigSettingsAnkiDeckNames: 'config-settings:anki-deck-names',
|
||||
getConfigSettingsAnkiDeckFieldNames: 'config-settings:anki-deck-field-names',
|
||||
getConfigSettingsAnkiDeckModelNames: 'config-settings:anki-deck-model-names',
|
||||
getConfigSettingsAnkiModelNames: 'config-settings:anki-model-names',
|
||||
getConfigSettingsAnkiModelFieldNames: 'config-settings:anki-model-field-names',
|
||||
},
|
||||
|
||||
@@ -16,15 +16,26 @@ function boolScriptOpt(value: boolean): 'yes' | 'no' {
|
||||
return value ? 'yes' : 'no';
|
||||
}
|
||||
|
||||
function sanitizeScriptOptValue(value: string): string {
|
||||
return value
|
||||
.replace(/,/g, ' ')
|
||||
.replace(/[\r\n]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function buildSubminerPluginRuntimeScriptOptParts(
|
||||
runtimeConfig: SubminerPluginRuntimeScriptOptConfig,
|
||||
fallbackAppPath: string,
|
||||
): string[] {
|
||||
const binaryPath = runtimeConfig.binaryPath?.trim() || fallbackAppPath;
|
||||
const binaryPath = sanitizeScriptOptValue(runtimeConfig.binaryPath?.trim() || fallbackAppPath);
|
||||
const socketPath = sanitizeScriptOptValue(runtimeConfig.socketPath);
|
||||
const backend = sanitizeScriptOptValue(runtimeConfig.backend);
|
||||
const aniskipButtonKey = sanitizeScriptOptValue(runtimeConfig.aniskipButtonKey);
|
||||
return [
|
||||
`subminer-binary_path=${binaryPath}`,
|
||||
`subminer-socket_path=${runtimeConfig.socketPath}`,
|
||||
`subminer-backend=${runtimeConfig.backend}`,
|
||||
`subminer-socket_path=${socketPath}`,
|
||||
`subminer-backend=${backend}`,
|
||||
`subminer-auto_start=${boolScriptOpt(runtimeConfig.autoStart)}`,
|
||||
`subminer-auto_start_visible_overlay=${boolScriptOpt(runtimeConfig.autoStartVisibleOverlay)}`,
|
||||
`subminer-auto_start_pause_until_ready=${boolScriptOpt(
|
||||
@@ -32,6 +43,6 @@ export function buildSubminerPluginRuntimeScriptOptParts(
|
||||
)}`,
|
||||
`subminer-texthooker_enabled=${boolScriptOpt(runtimeConfig.texthookerEnabled)}`,
|
||||
`subminer-aniskip_enabled=${boolScriptOpt(runtimeConfig.aniskipEnabled)}`,
|
||||
`subminer-aniskip_button_key=${runtimeConfig.aniskipButtonKey}`,
|
||||
`subminer-aniskip_button_key=${aniskipButtonKey}`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface ConfigSettingsAPI {
|
||||
openSettingsWindow(): Promise<boolean>;
|
||||
getAnkiDeckNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
|
||||
getAnkiDeckFieldNames(deckName: string, draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
|
||||
getAnkiDeckModelNames(deckName: string, draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
|
||||
getAnkiModelNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
|
||||
getAnkiModelFieldNames(
|
||||
modelName: string,
|
||||
|
||||
Reference in New Issue
Block a user