mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix: address config modal review feedback
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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()),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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',
|
||||||
]) {
|
]) {
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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}`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user