mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
feat: add manual known-word cache refresh path
- Add CLI command flag with non-GUI dispatch flow and OSD error handling. - Add runtime integration call and IPC hook so manual refresh works from command runner without app startup. - Add public AnkiIntegration manual refresh API with force refresh semantics and guard reuse. - Preserve default n+1 behavior by fixing config validation for malformed values and adding tests.
This commit is contained in:
@@ -16,11 +16,23 @@ SubMiner uses a service-oriented Electron architecture with a composition-orient
|
||||
|
||||
```text
|
||||
src/
|
||||
main.ts # Composition root — lifecycle wiring and state ownership
|
||||
main.ts # Entry point — delegates to src/main/ composition modules
|
||||
preload.ts # Electron preload bridge
|
||||
types.ts # Shared type definitions
|
||||
main/ # Composition root modules (extracted from main.ts)
|
||||
app-lifecycle.ts # Electron lifecycle event registration
|
||||
cli-runtime.ts # CLI command handling and dispatch
|
||||
dependencies.ts # Shared dependency builders for IPC/runtime
|
||||
ipc-mpv-command.ts # MPV command composition helpers
|
||||
ipc-runtime.ts # IPC channel registration and handlers
|
||||
overlay-runtime.ts # Overlay window/modal selection and state
|
||||
overlay-shortcuts-runtime.ts # Overlay keyboard shortcut handling
|
||||
startup.ts # Startup bootstrap flow (argv/env processing)
|
||||
startup-lifecycle.ts # App-ready initialization sequence
|
||||
state.ts # Application runtime state container
|
||||
subsync-runtime.ts # Subsync command orchestration
|
||||
core/
|
||||
services/ # ~55 focused service modules (see below)
|
||||
services/ # ~60 focused service modules (see below)
|
||||
utils/ # Pure helpers and coercion/config utilities
|
||||
cli/ # CLI parsing and help output
|
||||
config/ # Config schema, defaults, validation, template generation
|
||||
@@ -36,10 +48,10 @@ src/
|
||||
|
||||
### Service Layer (`src/core/services/`)
|
||||
|
||||
- **Startup** — `startup-service`, `app-lifecycle-service`
|
||||
- **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service`
|
||||
- **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service`
|
||||
- **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service`
|
||||
- **Startup** — `startup-service`, `app-lifecycle-service`, `app-ready-service`
|
||||
- **Overlay** — `overlay-manager-service`, `overlay-window-service`, `overlay-visibility-service`, `overlay-bridge-service`, `overlay-runtime-init-service`, `overlay-content-measurement-service`
|
||||
- **Shortcuts** — `shortcut-service`, `overlay-shortcut-service`, `overlay-shortcut-handler`, `shortcut-fallback-service`, `numeric-shortcut-service`, `numeric-shortcut-session-service`
|
||||
- **MPV** — `mpv-service`, `mpv-control-service`, `mpv-render-metrics-service`, `mpv-transport`, `mpv-protocol`, `mpv-state`, `mpv-properties`
|
||||
- **IPC** — `ipc-service`, `ipc-command-service`, `runtime-options-ipc-service`
|
||||
- **Mining** — `mining-service`, `field-grouping-service`, `field-grouping-overlay-service`, `anki-jimaku-service`, `anki-jimaku-ipc-service`
|
||||
- **Subtitles** — `subtitle-ws-service`, `subtitle-position-service`, `secondary-subtitle-service`, `tokenizer-service`
|
||||
|
||||
154
src/anki-integration.test.ts
Normal file
154
src/anki-integration.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { AnkiIntegration } from "./anki-integration";
|
||||
|
||||
interface IntegrationTestContext {
|
||||
integration: AnkiIntegration;
|
||||
calls: {
|
||||
findNotes: number;
|
||||
notesInfo: number;
|
||||
};
|
||||
stateDir: string;
|
||||
}
|
||||
|
||||
function createIntegrationTestContext(
|
||||
options: {
|
||||
highlightEnabled?: boolean;
|
||||
onFindNotes?: () => Promise<number[]>;
|
||||
onNotesInfo?: () => Promise<unknown[]>;
|
||||
stateDirPrefix?: string;
|
||||
} = {},
|
||||
): IntegrationTestContext {
|
||||
const calls = {
|
||||
findNotes: 0,
|
||||
notesInfo: 0,
|
||||
};
|
||||
|
||||
const stateDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), options.stateDirPrefix ?? "subminer-anki-integration-"),
|
||||
);
|
||||
const knownWordCacheStatePath = path.join(stateDir, "known-words-cache.json");
|
||||
|
||||
const client = {
|
||||
findNotes: async () => {
|
||||
calls.findNotes += 1;
|
||||
if (options.onFindNotes) {
|
||||
return options.onFindNotes();
|
||||
}
|
||||
return [] as number[];
|
||||
},
|
||||
notesInfo: async () => {
|
||||
calls.notesInfo += 1;
|
||||
if (options.onNotesInfo) {
|
||||
return options.onNotesInfo();
|
||||
}
|
||||
return [] as unknown[];
|
||||
},
|
||||
} as {
|
||||
findNotes: () => Promise<number[]>;
|
||||
notesInfo: () => Promise<unknown[]>;
|
||||
};
|
||||
|
||||
const integration = new AnkiIntegration(
|
||||
{
|
||||
nPlusOne: {
|
||||
highlightEnabled: options.highlightEnabled ?? true,
|
||||
},
|
||||
},
|
||||
{} as never,
|
||||
{} as never,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
knownWordCacheStatePath,
|
||||
);
|
||||
|
||||
const integrationWithClient = integration as unknown as {
|
||||
client: {
|
||||
findNotes: () => Promise<number[]>;
|
||||
notesInfo: () => Promise<unknown[]>;
|
||||
};
|
||||
};
|
||||
integrationWithClient.client = client;
|
||||
|
||||
const privateState = integration as unknown as {
|
||||
knownWordsScope: string;
|
||||
knownWordsLastRefreshedAtMs: number;
|
||||
};
|
||||
privateState.knownWordsScope = "is:note";
|
||||
privateState.knownWordsLastRefreshedAtMs = Date.now();
|
||||
|
||||
return {
|
||||
integration,
|
||||
calls,
|
||||
stateDir,
|
||||
};
|
||||
}
|
||||
|
||||
function cleanupIntegrationTestContext(ctx: IntegrationTestContext): void {
|
||||
fs.rmSync(ctx.stateDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
test("AnkiIntegration.refreshKnownWordCache bypasses stale checks", async () => {
|
||||
const ctx = createIntegrationTestContext();
|
||||
|
||||
try {
|
||||
await ctx.integration.refreshKnownWordCache();
|
||||
|
||||
assert.equal(ctx.calls.findNotes, 1);
|
||||
assert.equal(ctx.calls.notesInfo, 0);
|
||||
} finally {
|
||||
cleanupIntegrationTestContext(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
test("AnkiIntegration.refreshKnownWordCache skips work when highlight mode is disabled", async () => {
|
||||
const ctx = createIntegrationTestContext({
|
||||
highlightEnabled: false,
|
||||
stateDirPrefix: "subminer-anki-integration-disabled-",
|
||||
});
|
||||
|
||||
try {
|
||||
await ctx.integration.refreshKnownWordCache();
|
||||
|
||||
assert.equal(ctx.calls.findNotes, 0);
|
||||
assert.equal(ctx.calls.notesInfo, 0);
|
||||
} finally {
|
||||
cleanupIntegrationTestContext(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
test("AnkiIntegration.refreshKnownWordCache deduplicates concurrent refreshes", async () => {
|
||||
let releaseFindNotes: (() => void) | undefined;
|
||||
const findNotesPromise = new Promise<void>((resolve) => {
|
||||
releaseFindNotes = resolve;
|
||||
});
|
||||
|
||||
const ctx = createIntegrationTestContext({
|
||||
onFindNotes: async () => {
|
||||
await findNotesPromise;
|
||||
return [] as number[];
|
||||
},
|
||||
stateDirPrefix: "subminer-anki-integration-concurrent-",
|
||||
});
|
||||
|
||||
const first = ctx.integration.refreshKnownWordCache();
|
||||
await Promise.resolve();
|
||||
const second = ctx.integration.refreshKnownWordCache();
|
||||
|
||||
if (releaseFindNotes !== undefined) {
|
||||
releaseFindNotes();
|
||||
}
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
try {
|
||||
assert.equal(ctx.calls.findNotes, 1);
|
||||
assert.equal(ctx.calls.notesInfo, 0);
|
||||
} finally {
|
||||
cleanupIntegrationTestContext(ctx);
|
||||
}
|
||||
});
|
||||
@@ -223,7 +223,11 @@ export class AnkiIntegration {
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshKnownWords(): Promise<void> {
|
||||
async refreshKnownWordCache(): Promise<void> {
|
||||
return this.refreshKnownWords(true);
|
||||
}
|
||||
|
||||
private async refreshKnownWords(force = false): Promise<void> {
|
||||
if (!this.isKnownWordCacheEnabled()) {
|
||||
log.debug("Known-word cache refresh skipped; feature disabled");
|
||||
return;
|
||||
@@ -232,7 +236,7 @@ export class AnkiIntegration {
|
||||
log.debug("Known-word cache refresh skipped; already refreshing");
|
||||
return;
|
||||
}
|
||||
if (!this.isKnownWordCacheStale()) {
|
||||
if (!force && !this.isKnownWordCacheStale()) {
|
||||
log.debug("Known-word cache refresh skipped; cache is fresh");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,4 +41,9 @@ test("hasExplicitCommand and shouldStartApp preserve command intent", () => {
|
||||
const noCommand = parseArgs(["--verbose"]);
|
||||
assert.equal(hasExplicitCommand(noCommand), false);
|
||||
assert.equal(shouldStartApp(noCommand), false);
|
||||
|
||||
const refreshKnownWords = parseArgs(["--refresh-known-words"]);
|
||||
assert.equal(refreshKnownWords.help, false);
|
||||
assert.equal(hasExplicitCommand(refreshKnownWords), true);
|
||||
assert.equal(shouldStartApp(refreshKnownWords), false);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface CliArgs {
|
||||
mineSentence: boolean;
|
||||
mineSentenceMultiple: boolean;
|
||||
updateLastCardFromClipboard: boolean;
|
||||
refreshKnownWords: boolean;
|
||||
toggleSecondarySub: boolean;
|
||||
triggerFieldGrouping: boolean;
|
||||
triggerSubsync: boolean;
|
||||
@@ -55,6 +56,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
mineSentence: false,
|
||||
mineSentenceMultiple: false,
|
||||
updateLastCardFromClipboard: false,
|
||||
refreshKnownWords: false,
|
||||
toggleSecondarySub: false,
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
@@ -100,6 +102,7 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||
else if (arg === "--mine-sentence-multiple") args.mineSentenceMultiple = true;
|
||||
else if (arg === "--update-last-card-from-clipboard")
|
||||
args.updateLastCardFromClipboard = true;
|
||||
else if (arg === "--refresh-known-words") args.refreshKnownWords = true;
|
||||
else if (arg === "--toggle-secondary-sub") args.toggleSecondarySub = true;
|
||||
else if (arg === "--trigger-field-grouping")
|
||||
args.triggerFieldGrouping = true;
|
||||
@@ -181,6 +184,7 @@ export function hasExplicitCommand(args: CliArgs): boolean {
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.refreshKnownWords ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
|
||||
@@ -17,4 +17,5 @@ test("printHelp includes configured texthooker port", () => {
|
||||
|
||||
assert.match(output, /--help\s+Show this help/);
|
||||
assert.match(output, /default: 7777/);
|
||||
assert.match(output, /--refresh-known-words/);
|
||||
});
|
||||
|
||||
@@ -18,8 +18,9 @@ SubMiner CLI commands:
|
||||
--copy-subtitle-multiple Start multi-copy mode
|
||||
--mine-sentence Mine sentence card from current subtitle
|
||||
--mine-sentence-multiple Start multi-mine sentence mode
|
||||
--update-last-card-from-clipboard Update last card from clipboard
|
||||
--toggle-secondary-sub Cycle secondary subtitle mode
|
||||
--update-last-card-from-clipboard Update last card from clipboard
|
||||
--refresh-known-words Refresh known words cache now
|
||||
--toggle-secondary-sub Cycle secondary subtitle mode
|
||||
--trigger-field-grouping Trigger Kiku field grouping
|
||||
--trigger-subsync Run subtitle sync
|
||||
--mark-audio-card Mark last card as audio card
|
||||
|
||||
@@ -445,9 +445,14 @@ export class ConfigService {
|
||||
: isObject(ac.openRouter)
|
||||
? ac.openRouter
|
||||
: {};
|
||||
const { nPlusOne: _nPlusOneConfigFromAnkiConnect, ...ankiConnectWithoutNPlusOne } =
|
||||
ac as Record<string, unknown>;
|
||||
|
||||
resolved.ankiConnect = {
|
||||
...resolved.ankiConnect,
|
||||
...(isObject(ac) ? (ac as Partial<ResolvedConfig["ankiConnect"]>) : {}),
|
||||
...(isObject(ankiConnectWithoutNPlusOne)
|
||||
? (ankiConnectWithoutNPlusOne as Partial<ResolvedConfig["ankiConnect"]>)
|
||||
: {}),
|
||||
fields: {
|
||||
...resolved.ankiConnect.fields,
|
||||
...(isObject(ac.fields)
|
||||
@@ -598,17 +603,26 @@ export class ConfigService {
|
||||
behavior.nPlusOneHighlightEnabled,
|
||||
);
|
||||
if (legacyNPlusOneHighlightEnabled !== undefined) {
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled =
|
||||
legacyNPlusOneHighlightEnabled;
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled =
|
||||
legacyNPlusOneHighlightEnabled;
|
||||
warn(
|
||||
"ankiConnect.behavior.nPlusOneHighlightEnabled",
|
||||
behavior.nPlusOneHighlightEnabled,
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled,
|
||||
"Legacy key is deprecated; use ankiConnect.nPlusOne.highlightEnabled",
|
||||
);
|
||||
} else if (nPlusOneConfig.highlightEnabled !== undefined) {
|
||||
warn(
|
||||
"ankiConnect.nPlusOne.highlightEnabled",
|
||||
nPlusOneConfig.highlightEnabled,
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled,
|
||||
"Expected boolean.",
|
||||
);
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled =
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
|
||||
} else {
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled =
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
|
||||
resolved.ankiConnect.nPlusOne.highlightEnabled =
|
||||
DEFAULT_CONFIG.ankiConnect.nPlusOne.highlightEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,6 +748,7 @@ export class ConfigService {
|
||||
resolved.ankiConnect.nPlusOne.decks,
|
||||
"Expected an array of strings.",
|
||||
);
|
||||
resolved.ankiConnect.nPlusOne.decks = [];
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
export interface AnkiJimakuIpcDeps {
|
||||
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||
clearAnkiHistory: () => void;
|
||||
refreshKnownWords: () => Promise<void> | void;
|
||||
respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void;
|
||||
buildKikuMergePreview: (
|
||||
request: KikuMergePreviewRequest,
|
||||
@@ -55,6 +56,10 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
deps.clearAnkiHistory();
|
||||
});
|
||||
|
||||
ipcMain.on("anki:refresh-known-words", async () => {
|
||||
await deps.refreshKnownWords();
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"kiku:field-grouping-respond",
|
||||
(_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => {
|
||||
|
||||
@@ -107,6 +107,7 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
|
||||
const expected = [
|
||||
"setAnkiConnectEnabled",
|
||||
"clearAnkiHistory",
|
||||
"refreshKnownWords",
|
||||
"respondFieldGrouping",
|
||||
"buildKikuMergePreview",
|
||||
"getJimakuMediaInfo",
|
||||
@@ -124,6 +125,31 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("refreshKnownWords throws when integration is unavailable", async () => {
|
||||
const { registered } = createHarness();
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await registered.refreshKnownWords();
|
||||
},
|
||||
{ message: "AnkiConnect integration not enabled" },
|
||||
);
|
||||
});
|
||||
|
||||
test("refreshKnownWords delegates to integration", async () => {
|
||||
const { registered, state } = createHarness();
|
||||
let refreshed = 0;
|
||||
state.ankiIntegration = {
|
||||
refreshKnownWordCache: async () => {
|
||||
refreshed += 1;
|
||||
},
|
||||
};
|
||||
|
||||
await registered.refreshKnownWords();
|
||||
|
||||
assert.equal(refreshed, 1);
|
||||
});
|
||||
|
||||
test("setAnkiConnectEnabled disables active integration and broadcasts changes", () => {
|
||||
const { registered, state } = createHarness();
|
||||
let destroyed = 0;
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||
import { registerAnkiJimakuIpcHandlers } from "./anki-jimaku-ipc-service";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
|
||||
|
||||
export type RegisterAnkiJimakuIpcRuntimeHandler = (
|
||||
deps: AnkiJimakuIpcDeps,
|
||||
) => void;
|
||||
|
||||
interface MpvClientLike {
|
||||
connected: boolean;
|
||||
@@ -60,7 +64,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
|
||||
export function registerAnkiJimakuIpcRuntimeService(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
registerHandlers: typeof registerAnkiJimakuIpcHandlers = registerAnkiJimakuIpcHandlers,
|
||||
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
|
||||
): void {
|
||||
registerHandlers({
|
||||
setAnkiConnectEnabled: (enabled) => {
|
||||
@@ -108,6 +112,13 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
console.log("AnkiConnect subtitle timing history cleared");
|
||||
}
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
const integration = options.getAnkiIntegration();
|
||||
if (!integration) {
|
||||
throw new Error("AnkiConnect integration not enabled");
|
||||
}
|
||||
await integration.refreshKnownWordCache();
|
||||
},
|
||||
respondFieldGrouping: (choice) => {
|
||||
const resolver = options.getFieldGroupingResolver();
|
||||
if (resolver) {
|
||||
|
||||
@@ -26,6 +26,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
refreshKnownWords: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
@@ -106,6 +107,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push("updateLastCardFromClipboard");
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
calls.push("refreshKnownWords");
|
||||
},
|
||||
cycleSecondarySubMode: () => {
|
||||
calls.push("cycleSecondarySubMode");
|
||||
},
|
||||
@@ -288,3 +292,27 @@ test("handleCliCommandService handles visibility and utility command dispatches"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("handleCliCommandService runs refresh-known-words command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("refreshKnownWords"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async refresh-known-words errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
refreshKnownWords: async () => {
|
||||
throw new Error("refresh boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
calls.some((value) => value.startsWith("error:refreshKnownWords failed:")),
|
||||
);
|
||||
assert.ok(osd.some((value) => value.includes("Refresh known words failed: refresh boom")));
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface CliCommandServiceDeps {
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWords: () => Promise<void>;
|
||||
cycleSecondarySubMode: () => void;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
@@ -83,6 +84,7 @@ interface MiningCliRuntime {
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWords: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
@@ -159,6 +161,7 @@ export function createCliCommandDepsRuntimeService(
|
||||
startPendingMineSentenceMultiple:
|
||||
options.mining.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard,
|
||||
refreshKnownWords: options.mining.refreshKnownWords,
|
||||
cycleSecondarySubMode: options.ui.cycleSecondarySubMode,
|
||||
triggerFieldGrouping: options.mining.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
|
||||
@@ -208,6 +211,7 @@ export function handleCliCommandService(
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.refreshKnownWords ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
@@ -295,6 +299,13 @@ export function handleCliCommandService(
|
||||
"updateLastCardFromClipboard",
|
||||
"Update failed",
|
||||
);
|
||||
} else if (args.refreshKnownWords) {
|
||||
runAsyncWithOsd(
|
||||
() => deps.refreshKnownWords(),
|
||||
deps,
|
||||
"refreshKnownWords",
|
||||
"Refresh known words failed",
|
||||
);
|
||||
} else if (args.toggleSecondarySub) {
|
||||
deps.cycleSecondarySubMode();
|
||||
} else if (args.triggerFieldGrouping) {
|
||||
|
||||
@@ -704,6 +704,7 @@ function handleCliCommand(
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||
refreshKnownWordCache: () => refreshKnownWordCache(),
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
@@ -1110,6 +1111,14 @@ async function updateLastCardFromClipboard(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshKnownWordCache(): Promise<void> {
|
||||
if (!appState.ankiIntegration) {
|
||||
throw new Error("AnkiConnect integration not enabled");
|
||||
}
|
||||
|
||||
await appState.ankiIntegration.refreshKnownWordCache();
|
||||
}
|
||||
|
||||
async function triggerFieldGrouping(): Promise<void> {
|
||||
await triggerFieldGroupingService(
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface CliCommandRuntimeServiceContext {
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWordCache: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
@@ -70,12 +71,13 @@ function createCliCommandDepsFromContext(
|
||||
mining: {
|
||||
copyCurrentSubtitle: context.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: context.startPendingMultiCopy,
|
||||
mineSentenceCard: context.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: context.updateLastCardFromClipboard,
|
||||
triggerFieldGrouping: context.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: context.markLastCardAsAudioCard,
|
||||
mineSentenceCard: context.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: context.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: context.updateLastCardFromClipboard,
|
||||
refreshKnownWords: context.refreshKnownWordCache,
|
||||
triggerFieldGrouping: context.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: context.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: context.markLastCardAsAudioCard,
|
||||
},
|
||||
ui: {
|
||||
openYomitanSettings: context.openYomitanSettings,
|
||||
|
||||
@@ -145,6 +145,7 @@ export interface CliCommandRuntimeServiceDepsParams {
|
||||
CliCommandDepsRuntimeOptions["mining"]["startPendingMineSentenceMultiple"];
|
||||
updateLastCardFromClipboard:
|
||||
CliCommandDepsRuntimeOptions["mining"]["updateLastCardFromClipboard"];
|
||||
refreshKnownWords: CliCommandDepsRuntimeOptions["mining"]["refreshKnownWords"];
|
||||
triggerFieldGrouping: CliCommandDepsRuntimeOptions["mining"]["triggerFieldGrouping"];
|
||||
triggerSubsyncFromConfig:
|
||||
CliCommandDepsRuntimeOptions["mining"]["triggerSubsyncFromConfig"];
|
||||
@@ -273,6 +274,7 @@ export function createCliCommandRuntimeServiceDeps(
|
||||
mineSentenceCard: params.mining.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: params.mining.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: params.mining.updateLastCardFromClipboard,
|
||||
refreshKnownWords: params.mining.refreshKnownWords,
|
||||
triggerFieldGrouping: params.mining.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: params.mining.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: params.mining.markLastCardAsAudioCard,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
registerAnkiJimakuIpcRuntimeService,
|
||||
registerIpcHandlersService,
|
||||
} from "../core/services";
|
||||
import { registerAnkiJimakuIpcHandlers } from "../core/services/anki-jimaku-ipc-service";
|
||||
import {
|
||||
createAnkiJimakuIpcRuntimeServiceDeps,
|
||||
AnkiJimakuIpcRuntimeServiceDepsParams,
|
||||
@@ -34,6 +35,7 @@ export function registerAnkiJimakuIpcRuntimeServices(
|
||||
): void {
|
||||
registerAnkiJimakuIpcRuntimeService(
|
||||
createAnkiJimakuIpcRuntimeServiceDeps(params),
|
||||
registerAnkiJimakuIpcHandlers,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user