fix: address config modal review feedback

This commit is contained in:
2026-05-17 18:23:22 -07:00
parent c6537224f2
commit c369841827
17 changed files with 333 additions and 43 deletions
+30
View File
@@ -185,6 +185,36 @@ test('buildPluginRuntimeScriptOptParts emits config values that override plugin
); );
}); });
test('buildPluginRuntimeScriptOptParts strips script-option delimiters from string values', () => {
assert.deepEqual(
buildPluginRuntimeScriptOptParts(
{
socketPath: '/tmp/config.sock,subminer-auto_start=no\nother=yes',
binaryPath: '/opt/SubMiner,\nSubMiner.AppImage',
backend: 'x11',
autoStart: true,
autoStartVisibleOverlay: false,
autoStartPauseUntilReady: true,
texthookerEnabled: false,
aniskipEnabled: false,
aniskipButtonKey: 'F8,\nF9',
},
'/fallback/SubMiner.AppImage',
),
[
'subminer-binary_path=/opt/SubMiner SubMiner.AppImage',
'subminer-socket_path=/tmp/config.sock subminer-auto_start=no other=yes',
'subminer-backend=x11',
'subminer-auto_start=yes',
'subminer-auto_start_visible_overlay=no',
'subminer-auto_start_pause_until_ready=yes',
'subminer-texthooker_enabled=no',
'subminer-aniskip_enabled=no',
'subminer-aniskip_button_key=F8 F9',
],
);
});
test('getDefaultSocketPath returns Windows named pipe default', () => { test('getDefaultSocketPath returns Windows named pipe default', () => {
assert.equal(getDefaultSocketPath('win32'), '\\\\.\\pipe\\subminer-socket'); assert.equal(getDefaultSocketPath('win32'), '\\\\.\\pipe\\subminer-socket');
}); });
+1 -2
View File
@@ -2,12 +2,11 @@ import path from 'node:path';
import os from 'node:os'; import os from 'node:os';
import type { MpvBackend, MpvLaunchMode } from '../src/types/config.js'; import type { MpvBackend, MpvLaunchMode } from '../src/types/config.js';
import { resolveDefaultLogFilePath } from '../src/shared/log-files.js'; import { resolveDefaultLogFilePath } from '../src/shared/log-files.js';
import { getDefaultMpvSocketPath } from '../src/shared/mpv-socket-path.js';
export { VIDEO_EXTENSIONS } from '../src/shared/video-extensions.js'; export { VIDEO_EXTENSIONS } from '../src/shared/video-extensions.js';
export const ROFI_THEME_FILE = 'subminer.rasi'; export const ROFI_THEME_FILE = 'subminer.rasi';
export function getDefaultSocketPath(platform: NodeJS.Platform = process.platform): string { export function getDefaultSocketPath(platform: NodeJS.Platform = process.platform): string {
return getDefaultMpvSocketPath(platform); return platform === 'win32' ? '\\\\.\\pipe\\subminer-socket' : '/tmp/subminer-socket';
} }
export const DEFAULT_SOCKET_PATH = getDefaultSocketPath(); export const DEFAULT_SOCKET_PATH = getDefaultSocketPath();
+1 -1
View File
@@ -51,7 +51,7 @@
"test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts", "test:launcher:smoke:src": "bun test launcher/smoke.e2e.test.ts",
"test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src", "test:launcher:src": "bun test launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/mpv.test.ts launcher/picker.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/smoke.e2e.test.ts && bun run test:plugin:src",
"test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts", "test:core:src": "bun test src/preload-settings.test.ts src/settings/settings-anki-controls.test.ts src/cli/args.test.ts src/cli/help.test.ts src/shared/setup-state.test.ts src/core/services/cli-command.test.ts src/core/services/field-grouping-overlay.test.ts src/core/services/numeric-shortcut-session.test.ts src/core/services/secondary-subtitle.test.ts src/core/services/mpv-render-metrics.test.ts src/core/services/overlay-content-measurement.test.ts src/core/services/mpv-control.test.ts src/core/services/mpv.test.ts src/core/services/runtime-options-ipc.test.ts src/core/services/runtime-config.test.ts src/core/services/yomitan-extension-paths.test.ts src/core/services/yomitan-extension-loader.test.ts src/core/services/yomitan-settings.test.ts src/core/services/config-hot-reload.test.ts src/core/services/discord-presence.test.ts src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts src/core/services/subsync.test.ts src/core/services/overlay-bridge.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/stats-window.test.ts src/core/services/__tests__/stats-server.test.ts src/main/runtime/stats-server-routing.test.ts src/core/services/mining.test.ts src/core/services/anki-jimaku.test.ts src/core/services/jimaku-download-path.test.ts src/core/services/jellyfin.test.ts src/core/services/jellyfin-remote.test.ts src/core/services/immersion-tracker-service.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/app-ready.test.ts src/core/services/startup-bootstrap.test.ts src/core/services/subtitle-processing-controller.test.ts src/main/runtime/current-subtitle-snapshot.test.ts src/core/services/anilist/anilist-update-queue.test.ts src/core/services/anilist/rate-limiter.test.ts src/core/services/jlpt-token-filter.test.ts src/core/services/subtitle-position.test.ts src/core/utils/shortcut-config.test.ts src/main/runtime/startup-mode-flags.test.ts src/main/runtime/first-run-setup-plugin.test.ts src/main/runtime/first-run-setup-service.test.ts src/main/runtime/first-run-setup-window.test.ts src/main/runtime/command-line-launcher.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/app-ready-main-deps.test.ts src/main/runtime/update/appimage-updater.test.ts src/main/runtime/update/fetch-adapter.test.ts src/main/runtime/update/release-metadata-policy.test.ts src/main/runtime/update/update-dialogs.test.ts src/main/runtime/update/support-assets.test.ts src/renderer/error-recovery.test.ts src/renderer/subtitle-render.test.ts src/renderer/handlers/mouse.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/modals/jimaku.test.ts src/subsync/utils.test.ts src/main/anilist-url-guard.test.ts src/window-trackers/hyprland-tracker.test.ts src/window-trackers/x11-tracker.test.ts src/window-trackers/windows-helper.test.ts src/window-trackers/windows-tracker.test.ts launcher/config.test.ts launcher/config-domain-parsers.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts launcher/parse-args.test.ts launcher/main.test.ts launcher/commands/command-modules.test.ts launcher/commands/update-command.test.ts launcher/setup-gate.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts",
"test:core:dist": "bun test dist/settings/settings-anki-controls.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js", "test:core:dist": "bun test dist/preload-settings.test.js dist/settings/settings-anki-controls.test.js dist/cli/args.test.js dist/cli/help.test.js dist/core/services/cli-command.test.js dist/core/services/ipc.test.js dist/core/services/anki-jimaku-ipc.test.js dist/core/services/field-grouping-overlay.test.js dist/core/services/numeric-shortcut-session.test.js dist/core/services/secondary-subtitle.test.js dist/core/services/mpv-render-metrics.test.js dist/core/services/overlay-content-measurement.test.js dist/core/services/mpv-control.test.js dist/core/services/mpv.test.js dist/core/services/runtime-options-ipc.test.js dist/core/services/runtime-config.test.js dist/core/services/yomitan-extension-paths.test.js dist/core/services/config-hot-reload.test.js dist/core/services/discord-presence.test.js dist/core/services/tokenizer.test.js dist/core/services/tokenizer/annotation-stage.test.js dist/core/services/tokenizer/parser-selection-stage.test.js dist/core/services/tokenizer/parser-enrichment-stage.test.js dist/core/services/subsync.test.js dist/core/services/overlay-bridge.test.js dist/core/services/overlay-manager.test.js dist/core/services/overlay-shortcut-handler.test.js dist/core/services/mining.test.js dist/core/services/anki-jimaku.test.js dist/core/services/jimaku-download-path.test.js dist/core/services/jellyfin.test.js dist/core/services/jellyfin-remote.test.js dist/core/services/immersion-tracker-service.test.js dist/core/services/overlay-runtime-init.test.js dist/core/services/app-ready.test.js dist/core/services/startup-bootstrap.test.js dist/core/services/subtitle-processing-controller.test.js dist/main/runtime/current-subtitle-snapshot.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/anilist/anilist-update-queue.test.js dist/core/services/anilist/rate-limiter.test.js dist/core/services/jlpt-token-filter.test.js dist/core/services/subtitle-position.test.js dist/renderer/error-recovery.test.js dist/renderer/subtitle-render.test.js dist/renderer/handlers/mouse.test.js dist/renderer/handlers/keyboard.test.js dist/renderer/modals/jimaku.test.js dist/subsync/utils.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/hyprland-tracker.test.js dist/window-trackers/x11-tracker.test.js dist/window-trackers/windows-helper.test.js dist/window-trackers/windows-tracker.test.js",
"test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js", "test:core:smoke:dist": "bun test dist/cli/help.test.js dist/core/services/runtime-config.test.js dist/core/services/ipc.test.js dist/core/services/overlay-manager.test.js dist/core/services/anilist/anilist-token-store.test.js dist/core/services/startup-bootstrap.test.js dist/renderer/error-recovery.test.js dist/main/anilist-url-guard.test.js dist/window-trackers/x11-tracker.test.js",
"test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist", "test:smoke:dist": "bun run test:config:smoke:dist && bun run test:core:smoke:dist",
"test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts", "test:subtitle:src": "bun test src/core/services/subsync.test.ts src/subsync/utils.test.ts",
+33
View File
@@ -123,3 +123,36 @@ test('AnkiConnectClient derives field names from sampled notes in a deck', async
params: { notes: [3, 1, 2] }, 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
View File
@@ -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 escapedDeckName = deckName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const noteIds = await this.findNotes(`deck:"${escapedDeckName}"`, { maxRetries: 0 }); const noteIds = await this.findNotes(`deck:"${escapedDeckName}"`, { maxRetries: 0 });
if (noteIds.length === 0) { if (noteIds.length === 0) {
return []; 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>(); const fields = new Set<string>();
for (const noteInfo of noteInfos) { for (const noteInfo of noteInfos) {
const noteFields = noteInfo.fields; const noteFields = noteInfo.fields;
@@ -198,6 +205,18 @@ export class AnkiConnectClient {
return [...fields].sort(); 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>[]> { async notesInfo(noteIds: number[]): Promise<Record<string, unknown>[]> {
const result = await this.invoke('notesInfo', { notes: noteIds }); const result = await this.invoke('notesInfo', { notes: noteIds });
return (result as Record<string, unknown>[]) || []; return (result as Record<string, unknown>[]) || [];
@@ -31,6 +31,7 @@ export interface ConfigSettingsIpcChannels {
openConfigSettingsWindow: string; openConfigSettingsWindow: string;
getConfigSettingsAnkiDeckNames: string; getConfigSettingsAnkiDeckNames: string;
getConfigSettingsAnkiDeckFieldNames: string; getConfigSettingsAnkiDeckFieldNames: string;
getConfigSettingsAnkiDeckModelNames: string;
getConfigSettingsAnkiModelNames: string; getConfigSettingsAnkiModelNames: string;
getConfigSettingsAnkiModelFieldNames: string; getConfigSettingsAnkiModelFieldNames: string;
} }
@@ -38,6 +39,7 @@ export interface ConfigSettingsIpcChannels {
export interface ConfigSettingsAnkiClient { export interface ConfigSettingsAnkiClient {
deckNames(): Promise<string[]>; deckNames(): Promise<string[]>;
fieldNamesForDeck(deckName: string): Promise<string[]>; fieldNamesForDeck(deckName: string): Promise<string[]>;
modelNamesForDeck(deckName: string): Promise<string[]>;
modelNames(): Promise<string[]>; modelNames(): Promise<string[]>;
modelFieldNames(modelName: string): Promise<string[]>; modelFieldNames(modelName: string): Promise<string[]>;
} }
@@ -211,6 +213,15 @@ export function createConfigSettingsRuntime<TWindow extends ConfigSettingsWindow
: invalidAnkiListResult('Deck name is required.'); : 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) => deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsAnkiModelNames, (_event, draftUrl) =>
getAnkiList(draftUrl, (client) => client.modelNames()), getAnkiList(draftUrl, (client) => client.modelNames()),
); );
+1
View File
@@ -15,6 +15,7 @@ test('settings preload exposes Anki lookup helpers', () => {
for (const method of [ for (const method of [
'getAnkiDeckNames', 'getAnkiDeckNames',
'getAnkiDeckFieldNames', 'getAnkiDeckFieldNames',
'getAnkiDeckModelNames',
'getAnkiModelNames', 'getAnkiModelNames',
'getAnkiModelFieldNames', 'getAnkiModelFieldNames',
]) { ]) {
+6
View File
@@ -14,6 +14,7 @@ const SETTINGS_IPC_CHANNELS = {
openWindow: 'config:open-settings-window', openWindow: 'config:open-settings-window',
getAnkiDeckNames: 'config-settings:anki-deck-names', getAnkiDeckNames: 'config-settings:anki-deck-names',
getAnkiDeckFieldNames: 'config-settings:anki-deck-field-names', getAnkiDeckFieldNames: 'config-settings:anki-deck-field-names',
getAnkiDeckModelNames: 'config-settings:anki-deck-model-names',
getAnkiModelNames: 'config-settings:anki-model-names', getAnkiModelNames: 'config-settings:anki-model-names',
getAnkiModelFieldNames: 'config-settings:anki-model-field-names', getAnkiModelFieldNames: 'config-settings:anki-model-field-names',
} as const; } as const;
@@ -32,6 +33,11 @@ const configSettingsAPI: ConfigSettingsAPI = {
draftUrl?: string, draftUrl?: string,
): Promise<ConfigSettingsAnkiListResult> => ): Promise<ConfigSettingsAnkiListResult> =>
ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiDeckFieldNames, deckName, draftUrl), 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> => getAnkiModelNames: (draftUrl?: string): Promise<ConfigSettingsAnkiListResult> =>
ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiModelNames, draftUrl), ipcRenderer.invoke(SETTINGS_IPC_CHANNELS.getAnkiModelNames, draftUrl),
getAnkiModelFieldNames: ( getAnkiModelFieldNames: (
+22
View File
@@ -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 () => { test('session help chord resolver follows remapped session bindings', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness(); const { handlers, testGlobals } = createKeyboardHandlerHarness();
+19 -1
View File
@@ -953,13 +953,31 @@ export function createKeyboardHandlers(
syncKeyboardTokenSelection(); 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> { async function setupMpvInputForwarding(): Promise<void> {
installMpvInputForwardingListeners(); installMpvInputForwardingListeners();
syncKeyboardTokenSelection(); syncKeyboardTokenSelection();
let configLoadSettled = false; let configLoadSettled = false;
let configLoadError: unknown = null; let configLoadError: unknown = null;
const configLoad = loadMpvInputForwardingConfig().then( const configLoad = loadMpvInputForwardingConfigWithRetry().then(
() => { () => {
configLoadSettled = true; configLoadSettled = true;
}, },
+33 -1
View File
@@ -3,7 +3,11 @@ import test from 'node:test';
import type { ElectronAPI, SubtitleSidebarSnapshot } from '../../types'; import type { ElectronAPI, SubtitleSidebarSnapshot } from '../../types';
import { createRendererState } from '../state.js'; import { createRendererState } from '../state.js';
import { createSubtitleSidebarModal, findActiveSubtitleCueIndex } from './subtitle-sidebar.js'; import {
applySidebarCssDeclarations,
createSubtitleSidebarModal,
findActiveSubtitleCueIndex,
} from './subtitle-sidebar.js';
function createClassList(initialTokens: string[] = []) { function createClassList(initialTokens: string[] = []) {
const tokens = new Set(initialTokens); 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); 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 () => { test('subtitle sidebar modal opens from snapshot and clicking cue seeks playback', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown }; const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window; const previousWindow = globals.window;
+20 -4
View File
@@ -8,6 +8,7 @@ const CLICK_SEEK_OFFSET_SEC = 0.08;
const SNAPSHOT_POLL_INTERVAL_MS = 80; const SNAPSHOT_POLL_INTERVAL_MS = 80;
const EMBEDDED_SIDEBAR_MIN_WIDTH_PX = 240; const EMBEDDED_SIDEBAR_MIN_WIDTH_PX = 240;
const EMBEDDED_SIDEBAR_MAX_RATIO = 0.45; const EMBEDDED_SIDEBAR_MAX_RATIO = 0.45;
const appliedSidebarCssKeys = new WeakMap<HTMLElement, Set<string>>();
function nowForUiTiming(): number { function nowForUiTiming(): number {
if (typeof performance !== 'undefined' && typeof performance.now === 'function') { 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')}`; return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
} }
function applySidebarCssDeclarations( export function applySidebarCssDeclarations(
target: HTMLElement, target: HTMLElement,
declarations: Record<string, string>, declarations: Record<string, string>,
): void { ): void {
const targetStyle = (target as HTMLElement & { style?: CSSStyleDeclaration }).style; const targetStyle = (target as HTMLElement & { style?: CSSStyleDeclaration }).style;
if (!targetStyle) return; 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)) { for (const [property, rawValue] of Object.entries(declarations)) {
const value = rawValue.trim(); const value = rawValue.trim();
if (value.length === 0) continue; if (value.length === 0) continue;
if (property.includes('-')) { if (property.includes('-')) {
targetStyle.setProperty(property, value); targetStyle.setProperty(property, value);
continue; } else {
styleTarget[property] = value;
} }
const styleTarget = targetStyle as unknown as Record<string, string>; nextKeys.add(property);
styleTarget[property] = value;
} }
appliedSidebarCssKeys.set(target, nextKeys);
} }
export function findActiveSubtitleCueIndex( export function findActiveSubtitleCueIndex(
+9 -15
View File
@@ -2,25 +2,22 @@ import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as ankiControls from './settings-anki-controls'; 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( assert.equal(
ankiControls.selectPreferredNoteFieldModelName(['Lapis Morph', 'Kiku'], 'Lapis Morph'), 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( assert.equal(
ankiControls.selectPreferredNoteFieldModelName(['Lapis Morph', 'Kiku'], 'lapis morph'), ankiControls.selectPreferredNoteFieldModelName(['Lapis Morph', 'Kiku'], 'lapis morph'),
'Kiku', 'Lapis Morph',
); );
}); });
test('note field model preference prefers exact Lapis when Kiku is unavailable', () => { test('note field model preference prefers exact Lapis when Kiku is unavailable', () => {
assert.equal( assert.equal(ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis'], ''), 'Lapis');
ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis'], ''),
'Lapis',
);
}); });
test('note field model preference prefers exact Kiku over exact 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', () => { test('note field model preference does not treat partial Kiku matches as Kiku', () => {
assert.equal( assert.equal(ankiControls.selectPreferredNoteFieldModelName(['Kikuchi', 'Mining'], ''), '');
ankiControls.selectPreferredNoteFieldModelName(['Kikuchi', 'Lapis Morph'], 'Lapis Morph'),
'',
);
}); });
test('note field model preference does not treat partial Lapis matches as Lapis', () => { test('note field model preference accepts partial Lapis matches', () => {
assert.equal( assert.equal(
ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis Morph'], 'Lapis Morph'), ankiControls.selectPreferredNoteFieldModelName(['Mining', 'Lapis Morph'], ''),
'', 'Lapis Morph',
); );
}); });
+109 -13
View File
@@ -9,6 +9,8 @@ const state: {
deckFieldNames: Map<string, string[]>; deckFieldNames: Map<string, string[]>;
deckFieldNamesLoading: Set<string>; deckFieldNamesLoading: Set<string>;
deckFieldNamesErrors: Map<string, string>; deckFieldNamesErrors: Map<string, string>;
deckModelNames: Map<string, string[]>;
deckModelNamesLoading: Map<string, Promise<string[]>>;
modelNames: string[] | null; modelNames: string[] | null;
modelNamesLoading: boolean; modelNamesLoading: boolean;
modelNamesError: string | null; modelNamesError: string | null;
@@ -25,6 +27,8 @@ const state: {
deckFieldNames: new Map(), deckFieldNames: new Map(),
deckFieldNamesLoading: new Set(), deckFieldNamesLoading: new Set(),
deckFieldNamesErrors: new Map(), deckFieldNamesErrors: new Map(),
deckModelNames: new Map(),
deckModelNamesLoading: new Map(),
modelNames: null, modelNames: null,
modelNamesLoading: false, modelNamesLoading: false,
modelNamesError: null, modelNamesError: null,
@@ -55,16 +59,26 @@ export function initializeAnkiControls(values: Record<string, ConfigSettingsSnap
export function selectPreferredNoteFieldModelName( export function selectPreferredNoteFieldModelName(
modelNames: readonly string[], modelNames: readonly string[],
_currentModelName = '', currentModelName = '',
): string { ): 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'); const exactKiku = modelNames.find((name) => name.toLowerCase() === 'kiku');
if (exactKiku) { if (exactKiku) {
return exactKiku; return exactKiku;
} }
const exactLapis = modelNames.find((name) => name.toLowerCase() === 'lapis'); const lapis = modelNames.find((name) => name.toLowerCase().includes('lapis'));
if (exactLapis) { if (lapis) {
return exactLapis; return lapis;
} }
return ''; return '';
@@ -106,6 +120,11 @@ function getDraftAnkiConnectUrl(context: SettingsControlContext): string | undef
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; 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 { function syncAnkiConnectUrl(draftUrl: string | undefined): void {
const nextUrl = draftUrl ?? ''; const nextUrl = draftUrl ?? '';
if (state.ankiConnectUrl === nextUrl) { if (state.ankiConnectUrl === nextUrl) {
@@ -116,6 +135,8 @@ function syncAnkiConnectUrl(draftUrl: string | undefined): void {
state.deckNamesLoading || state.deckNamesLoading ||
state.deckFieldNames.size > 0 || state.deckFieldNames.size > 0 ||
state.deckFieldNamesLoading.size > 0 || state.deckFieldNamesLoading.size > 0 ||
state.deckModelNames.size > 0 ||
state.deckModelNamesLoading.size > 0 ||
state.modelNames !== null || state.modelNames !== null ||
state.modelNamesLoading || state.modelNamesLoading ||
state.modelFieldNames.size > 0 || state.modelFieldNames.size > 0 ||
@@ -131,6 +152,8 @@ function syncAnkiConnectUrl(draftUrl: string | undefined): void {
state.deckFieldNames.clear(); state.deckFieldNames.clear();
state.deckFieldNamesLoading.clear(); state.deckFieldNamesLoading.clear();
state.deckFieldNamesErrors.clear(); state.deckFieldNamesErrors.clear();
state.deckModelNames.clear();
state.deckModelNamesLoading.clear();
state.modelNames = null; state.modelNames = null;
state.modelNamesLoading = false; state.modelNamesLoading = false;
state.modelNamesError = null; 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); 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; const requestUrl = state.ankiConnectUrl;
state.modelNamesLoading = true; state.modelNamesLoading = true;
try { try {
@@ -217,10 +316,7 @@ async function loadAnkiModelNames(draftUrl?: string): Promise<void> {
state.modelNames = uniqueSorted(result.values); state.modelNames = uniqueSorted(result.values);
state.modelNamesError = null; state.modelNamesError = null;
if (!state.noteFieldModelNameManuallySelected) { if (!state.noteFieldModelNameManuallySelected) {
state.noteFieldModelName = selectPreferredNoteFieldModelName( await updatePreferredNoteFieldModelName(state.modelNames, deckName, draftUrl, requestUrl);
state.modelNames,
state.noteFieldModelName,
);
} }
} else { } else {
state.modelNames = []; state.modelNames = [];
@@ -283,7 +379,7 @@ export function renderAnkiNoteTypeInput(
field: ConfigSettingsField, field: ConfigSettingsField,
): HTMLElement { ): HTMLElement {
const draftUrl = getDraftAnkiConnectUrl(context); const draftUrl = getDraftAnkiConnectUrl(context);
void loadAnkiModelNames(draftUrl); void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
const currentValue = context.valueForField(field); const currentValue = context.valueForField(field);
const current = typeof currentValue === 'string' ? currentValue : ''; const current = typeof currentValue === 'string' ? currentValue : '';
const select = createElement('select', 'config-input') as HTMLSelectElement; const select = createElement('select', 'config-input') as HTMLSelectElement;
@@ -312,7 +408,7 @@ export function renderAnkiFieldInput(
field: ConfigSettingsField, field: ConfigSettingsField,
): HTMLElement { ): HTMLElement {
const draftUrl = getDraftAnkiConnectUrl(context); const draftUrl = getDraftAnkiConnectUrl(context);
void loadAnkiModelNames(draftUrl); void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
if (state.noteFieldModelName) { if (state.noteFieldModelName) {
void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl); void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl);
} }
@@ -350,7 +446,7 @@ export function renderAnkiFieldInput(
export function renderNoteFieldModelPicker(context: SettingsControlContext): HTMLElement { export function renderNoteFieldModelPicker(context: SettingsControlContext): HTMLElement {
const draftUrl = getDraftAnkiConnectUrl(context); const draftUrl = getDraftAnkiConnectUrl(context);
void loadAnkiModelNames(draftUrl); void loadAnkiModelNames(draftUrl, getDraftAnkiDeckName(context));
if (state.noteFieldModelName) { if (state.noteFieldModelName) {
void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl); void loadAnkiModelFieldNames(state.noteFieldModelName, draftUrl);
} }
+1
View File
@@ -103,6 +103,7 @@ export const IPC_CHANNELS = {
openConfigSettingsWindow: 'config:open-settings-window', openConfigSettingsWindow: 'config:open-settings-window',
getConfigSettingsAnkiDeckNames: 'config-settings:anki-deck-names', getConfigSettingsAnkiDeckNames: 'config-settings:anki-deck-names',
getConfigSettingsAnkiDeckFieldNames: 'config-settings:anki-deck-field-names', getConfigSettingsAnkiDeckFieldNames: 'config-settings:anki-deck-field-names',
getConfigSettingsAnkiDeckModelNames: 'config-settings:anki-deck-model-names',
getConfigSettingsAnkiModelNames: 'config-settings:anki-model-names', getConfigSettingsAnkiModelNames: 'config-settings:anki-model-names',
getConfigSettingsAnkiModelFieldNames: 'config-settings:anki-model-field-names', getConfigSettingsAnkiModelFieldNames: 'config-settings:anki-model-field-names',
}, },
+15 -4
View File
@@ -16,15 +16,26 @@ function boolScriptOpt(value: boolean): 'yes' | 'no' {
return value ? '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( export function buildSubminerPluginRuntimeScriptOptParts(
runtimeConfig: SubminerPluginRuntimeScriptOptConfig, runtimeConfig: SubminerPluginRuntimeScriptOptConfig,
fallbackAppPath: string, fallbackAppPath: string,
): 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 [ return [
`subminer-binary_path=${binaryPath}`, `subminer-binary_path=${binaryPath}`,
`subminer-socket_path=${runtimeConfig.socketPath}`, `subminer-socket_path=${socketPath}`,
`subminer-backend=${runtimeConfig.backend}`, `subminer-backend=${backend}`,
`subminer-auto_start=${boolScriptOpt(runtimeConfig.autoStart)}`, `subminer-auto_start=${boolScriptOpt(runtimeConfig.autoStart)}`,
`subminer-auto_start_visible_overlay=${boolScriptOpt(runtimeConfig.autoStartVisibleOverlay)}`, `subminer-auto_start_visible_overlay=${boolScriptOpt(runtimeConfig.autoStartVisibleOverlay)}`,
`subminer-auto_start_pause_until_ready=${boolScriptOpt( `subminer-auto_start_pause_until_ready=${boolScriptOpt(
@@ -32,6 +43,6 @@ export function buildSubminerPluginRuntimeScriptOptParts(
)}`, )}`,
`subminer-texthooker_enabled=${boolScriptOpt(runtimeConfig.texthookerEnabled)}`, `subminer-texthooker_enabled=${boolScriptOpt(runtimeConfig.texthookerEnabled)}`,
`subminer-aniskip_enabled=${boolScriptOpt(runtimeConfig.aniskipEnabled)}`, `subminer-aniskip_enabled=${boolScriptOpt(runtimeConfig.aniskipEnabled)}`,
`subminer-aniskip_button_key=${runtimeConfig.aniskipButtonKey}`, `subminer-aniskip_button_key=${aniskipButtonKey}`,
]; ];
} }
+1
View File
@@ -90,6 +90,7 @@ export interface ConfigSettingsAPI {
openSettingsWindow(): Promise<boolean>; openSettingsWindow(): Promise<boolean>;
getAnkiDeckNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>; getAnkiDeckNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
getAnkiDeckFieldNames(deckName: string, draftUrl?: string): Promise<ConfigSettingsAnkiListResult>; getAnkiDeckFieldNames(deckName: string, draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
getAnkiDeckModelNames(deckName: string, draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
getAnkiModelNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>; getAnkiModelNames(draftUrl?: string): Promise<ConfigSettingsAnkiListResult>;
getAnkiModelFieldNames( getAnkiModelFieldNames(
modelName: string, modelName: string,