mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 12:55:16 -07:00
feat(character-dictionary): add manager modal and scope name matching to current media (#86)
This commit is contained in:
@@ -152,8 +152,5 @@ test('auto sync notifications fall back to desktop when startup sequencer cannot
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'sequencer:importing:importing',
|
||||
'desktop:SubMiner:importing',
|
||||
]);
|
||||
assert.deepEqual(calls, ['sequencer:importing:importing', 'desktop:SubMiner:importing']);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,12 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import test from 'node:test';
|
||||
import { createCharacterDictionaryAutoSyncRuntimeService } from './character-dictionary-auto-sync';
|
||||
import {
|
||||
createCharacterDictionaryAutoSyncRuntimeService,
|
||||
getCharacterDictionaryManagerSnapshot,
|
||||
moveCharacterDictionaryManagedEntry,
|
||||
removeCharacterDictionaryManagedEntry,
|
||||
} from './character-dictionary-auto-sync';
|
||||
|
||||
function makeTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-char-dict-auto-sync-'));
|
||||
@@ -17,6 +22,94 @@ function createDeferred<T>(): { promise: Promise<T>; resolve: (value: T) => void
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
test('character dictionary manager snapshots, reorders, and removes MRU entries', () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const statePath = path.join(userDataPath, 'character-dictionaries', 'auto-sync-state.json');
|
||||
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
activeMediaIds: ['21202 - KonoSuba', '115230 - Tower of God', '130298 - Eminence'],
|
||||
mergedRevision: 'rev-1',
|
||||
mergedDictionaryTitle: 'SubMiner Character Dictionary',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.deepEqual(getCharacterDictionaryManagerSnapshot(userDataPath).entries, [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{ mediaId: 115230, label: '115230 - Tower of God', title: 'Tower of God', current: false },
|
||||
{ mediaId: 130298, label: '130298 - Eminence', title: 'Eminence', current: false },
|
||||
]);
|
||||
|
||||
assert.deepEqual(moveCharacterDictionaryManagedEntry(userDataPath, 130298, -1), {
|
||||
ok: true,
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{ mediaId: 130298, label: '130298 - Eminence', title: 'Eminence', current: false },
|
||||
{ mediaId: 115230, label: '115230 - Tower of God', title: 'Tower of God', current: false },
|
||||
],
|
||||
rebuildRequired: true,
|
||||
});
|
||||
const reorderedState = JSON.parse(fs.readFileSync(statePath, 'utf8')) as {
|
||||
mergedRevision: string | null;
|
||||
};
|
||||
assert.equal(reorderedState.mergedRevision, null);
|
||||
|
||||
assert.deepEqual(removeCharacterDictionaryManagedEntry(userDataPath, 115230), {
|
||||
ok: true,
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: true },
|
||||
{ mediaId: 130298, label: '130298 - Eminence', title: 'Eminence', current: false },
|
||||
],
|
||||
rebuildRequired: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('character dictionary manager protects the actual current media after LRU reorder', () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const statePath = path.join(userDataPath, 'character-dictionaries', 'auto-sync-state.json');
|
||||
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
statePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
activeMediaIds: ['21202 - KonoSuba', '115230 - Tower of God'],
|
||||
mergedRevision: 'rev-1',
|
||||
mergedDictionaryTitle: 'SubMiner Character Dictionary',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
assert.deepEqual(getCharacterDictionaryManagerSnapshot(userDataPath, 115230).entries, [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: false },
|
||||
{ mediaId: 115230, label: '115230 - Tower of God', title: 'Tower of God', current: true },
|
||||
]);
|
||||
assert.deepEqual(moveCharacterDictionaryManagedEntry(userDataPath, 115230, -1, 115230), {
|
||||
ok: false,
|
||||
message: 'The current anime stays anchored while you are watching it.',
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: false },
|
||||
{ mediaId: 115230, label: '115230 - Tower of God', title: 'Tower of God', current: true },
|
||||
],
|
||||
});
|
||||
assert.deepEqual(removeCharacterDictionaryManagedEntry(userDataPath, 115230, 115230), {
|
||||
ok: false,
|
||||
message: 'The current anime stays loaded while you are watching it.',
|
||||
entries: [
|
||||
{ mediaId: 21202, label: '21202 - KonoSuba', title: 'KonoSuba', current: false },
|
||||
{ mediaId: 115230, label: '115230 - Tower of God', title: 'Tower of God', current: true },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('auto sync imports merged dictionary and persists MRU state', async () => {
|
||||
const userDataPath = makeTempDir();
|
||||
const imported: string[] = [];
|
||||
|
||||
@@ -24,6 +24,21 @@ type AutoSyncDictionaryInfo = {
|
||||
revision?: string | number;
|
||||
};
|
||||
|
||||
export interface CharacterDictionaryManagerEntry {
|
||||
mediaId: number;
|
||||
label: string;
|
||||
title: string;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
export interface CharacterDictionaryManagerSnapshot {
|
||||
entries: CharacterDictionaryManagerEntry[];
|
||||
}
|
||||
|
||||
export type CharacterDictionaryManagerMutationResult =
|
||||
| (CharacterDictionaryManagerSnapshot & { ok: true; rebuildRequired?: boolean })
|
||||
| { ok: false; message: string; entries: CharacterDictionaryManagerEntry[] };
|
||||
|
||||
export interface CharacterDictionaryAutoSyncConfig {
|
||||
enabled: boolean;
|
||||
maxLoaded: number;
|
||||
@@ -154,6 +169,167 @@ function writeAutoSyncState(statePath: string, state: AutoSyncState): void {
|
||||
fs.writeFileSync(statePath, JSON.stringify(persistedState, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function getAutoSyncStatePath(userDataPath: string): string {
|
||||
return path.join(userDataPath, 'character-dictionaries', 'auto-sync-state.json');
|
||||
}
|
||||
|
||||
function parseActiveMediaTitle(entry: AutoSyncMediaEntry): string {
|
||||
const prefix = `${entry.mediaId} - `;
|
||||
if (entry.label.startsWith(prefix)) {
|
||||
return entry.label.slice(prefix.length).trim();
|
||||
}
|
||||
return entry.label === String(entry.mediaId) ? '' : entry.label.trim();
|
||||
}
|
||||
|
||||
function resolveCurrentManagerMediaId(
|
||||
state: AutoSyncState,
|
||||
currentMediaId?: number | null,
|
||||
): number | null {
|
||||
const normalizedCurrentMediaId =
|
||||
typeof currentMediaId === 'number' ? normalizeMediaId(currentMediaId) : null;
|
||||
if (normalizedCurrentMediaId !== null) return normalizedCurrentMediaId;
|
||||
return state.activeMediaIds[0]?.mediaId ?? null;
|
||||
}
|
||||
|
||||
function toManagerEntries(
|
||||
state: AutoSyncState,
|
||||
currentMediaId?: number | null,
|
||||
): CharacterDictionaryManagerEntry[] {
|
||||
const resolvedCurrentMediaId = resolveCurrentManagerMediaId(state, currentMediaId);
|
||||
return state.activeMediaIds.map((entry, index) => ({
|
||||
mediaId: entry.mediaId,
|
||||
label: entry.label,
|
||||
title: parseActiveMediaTitle(entry),
|
||||
current:
|
||||
resolvedCurrentMediaId !== null ? entry.mediaId === resolvedCurrentMediaId : index === 0,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getCharacterDictionaryManagerSnapshot(
|
||||
userDataPath: string,
|
||||
currentMediaId?: number | null,
|
||||
): CharacterDictionaryManagerSnapshot {
|
||||
return {
|
||||
entries: toManagerEntries(
|
||||
readAutoSyncState(getAutoSyncStatePath(userDataPath)),
|
||||
currentMediaId,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function moveCharacterDictionaryManagedEntry(
|
||||
userDataPath: string,
|
||||
mediaId: number,
|
||||
direction: 1 | -1,
|
||||
currentMediaId?: number | null,
|
||||
): CharacterDictionaryManagerMutationResult {
|
||||
const statePath = getAutoSyncStatePath(userDataPath);
|
||||
const state = readAutoSyncState(statePath);
|
||||
const managerEntries = toManagerEntries(state, currentMediaId);
|
||||
const index = state.activeMediaIds.findIndex((entry) => entry.mediaId === mediaId);
|
||||
if (index < 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'Character dictionary entry not found.',
|
||||
entries: managerEntries,
|
||||
};
|
||||
}
|
||||
if (managerEntries[index]?.current) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'The current anime stays anchored while you are watching it.',
|
||||
entries: managerEntries,
|
||||
};
|
||||
}
|
||||
const targetIndex = Math.min(state.activeMediaIds.length - 1, Math.max(0, index + direction));
|
||||
if (targetIndex === index) {
|
||||
return { ok: true, entries: managerEntries };
|
||||
}
|
||||
const nextActiveMediaIds = [...state.activeMediaIds];
|
||||
const [entry] = nextActiveMediaIds.splice(index, 1);
|
||||
if (entry) {
|
||||
nextActiveMediaIds.splice(targetIndex, 0, entry);
|
||||
}
|
||||
const nextState = { ...state, activeMediaIds: nextActiveMediaIds, mergedRevision: null };
|
||||
writeAutoSyncState(statePath, nextState);
|
||||
return { ok: true, entries: toManagerEntries(nextState, currentMediaId), rebuildRequired: true };
|
||||
}
|
||||
|
||||
export function removeCharacterDictionaryManagedEntry(
|
||||
userDataPath: string,
|
||||
mediaId: number,
|
||||
currentMediaId?: number | null,
|
||||
): CharacterDictionaryManagerMutationResult {
|
||||
const statePath = getAutoSyncStatePath(userDataPath);
|
||||
const state = readAutoSyncState(statePath);
|
||||
const managerEntries = toManagerEntries(state, currentMediaId);
|
||||
const index = state.activeMediaIds.findIndex((entry) => entry.mediaId === mediaId);
|
||||
if (index < 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'Character dictionary entry not found.',
|
||||
entries: managerEntries,
|
||||
};
|
||||
}
|
||||
if (managerEntries[index]?.current) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'The current anime stays loaded while you are watching it.',
|
||||
entries: managerEntries,
|
||||
};
|
||||
}
|
||||
const nextState = {
|
||||
...state,
|
||||
activeMediaIds: state.activeMediaIds.filter((entry) => entry.mediaId !== mediaId),
|
||||
mergedRevision: null,
|
||||
};
|
||||
writeAutoSyncState(statePath, nextState);
|
||||
return { ok: true, entries: toManagerEntries(nextState, currentMediaId), rebuildRequired: true };
|
||||
}
|
||||
|
||||
export function replaceCharacterDictionaryManagedEntry(
|
||||
userDataPath: string,
|
||||
mediaId: number,
|
||||
replacement: { mediaId: number; mediaTitle: string },
|
||||
): CharacterDictionaryManagerMutationResult {
|
||||
const statePath = getAutoSyncStatePath(userDataPath);
|
||||
const state = readAutoSyncState(statePath);
|
||||
const index = state.activeMediaIds.findIndex((entry) => entry.mediaId === mediaId);
|
||||
if (index < 0) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'Character dictionary entry not found.',
|
||||
entries: toManagerEntries(state),
|
||||
};
|
||||
}
|
||||
const normalizedReplacementMediaId = normalizeMediaId(replacement.mediaId);
|
||||
const mediaTitle = replacement.mediaTitle.trim();
|
||||
if (normalizedReplacementMediaId === null || !mediaTitle) {
|
||||
return {
|
||||
ok: false,
|
||||
message: 'Invalid replacement AniList media.',
|
||||
entries: toManagerEntries(state),
|
||||
};
|
||||
}
|
||||
const replacementEntry = {
|
||||
mediaId: normalizedReplacementMediaId,
|
||||
label: buildActiveMediaLabel(normalizedReplacementMediaId, mediaTitle),
|
||||
};
|
||||
const nextActiveMediaIds = state.activeMediaIds
|
||||
.map((entry, entryIndex) => (entryIndex === index ? replacementEntry : entry))
|
||||
.filter(
|
||||
(entry, entryIndex, entries) =>
|
||||
entries.findIndex((candidate) => candidate.mediaId === entry.mediaId) === entryIndex,
|
||||
);
|
||||
const nextState = {
|
||||
...state,
|
||||
activeMediaIds: nextActiveMediaIds,
|
||||
mergedRevision: null,
|
||||
};
|
||||
writeAutoSyncState(statePath, nextState);
|
||||
return { ok: true, entries: toManagerEntries(nextState), rebuildRequired: true };
|
||||
}
|
||||
|
||||
function arraysEqual(left: number[], right: number[]): boolean {
|
||||
if (left.length !== right.length) return false;
|
||||
for (let i = 0; i < left.length; i += 1) {
|
||||
@@ -205,9 +381,10 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
): {
|
||||
scheduleSync: () => void;
|
||||
runSyncNow: () => Promise<void>;
|
||||
getCurrentMediaId: () => number | null;
|
||||
} {
|
||||
const dictionariesDir = path.join(deps.userDataPath, 'character-dictionaries');
|
||||
const statePath = path.join(dictionariesDir, 'auto-sync-state.json');
|
||||
const statePath = getAutoSyncStatePath(deps.userDataPath);
|
||||
const schedule = deps.schedule ?? ((fn, delayMs) => setTimeout(fn, delayMs));
|
||||
const clearSchedule = deps.clearSchedule ?? ((timer) => clearTimeout(timer));
|
||||
const debounceMs = 800;
|
||||
@@ -216,6 +393,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let syncInFlight = false;
|
||||
let runQueued = false;
|
||||
let activeCurrentMediaId: number | null = null;
|
||||
|
||||
const withOperationTimeout = async <T>(label: string, promise: Promise<T>): Promise<T> => {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -238,6 +416,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
const runSyncOnce = async (): Promise<void> => {
|
||||
const config = deps.getConfig();
|
||||
if (!config.enabled) {
|
||||
activeCurrentMediaId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,6 +429,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
onChecking: ({ mediaId, mediaTitle }) => {
|
||||
currentMediaId = mediaId;
|
||||
currentMediaTitle = mediaTitle;
|
||||
activeCurrentMediaId = mediaId;
|
||||
deps.onSyncStatus?.({
|
||||
phase: 'checking',
|
||||
mediaId,
|
||||
@@ -260,6 +440,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
onGenerating: ({ mediaId, mediaTitle }) => {
|
||||
currentMediaId = mediaId;
|
||||
currentMediaTitle = mediaTitle;
|
||||
activeCurrentMediaId = mediaId;
|
||||
deps.onSyncStatus?.({
|
||||
phase: 'generating',
|
||||
mediaId,
|
||||
@@ -270,6 +451,7 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
});
|
||||
currentMediaId = snapshot.mediaId;
|
||||
currentMediaTitle = snapshot.mediaTitle;
|
||||
activeCurrentMediaId = snapshot.mediaId;
|
||||
const state = readAutoSyncState(statePath);
|
||||
const staleMediaIds = new Set(
|
||||
(snapshot.staleMediaIds ?? [])
|
||||
@@ -453,5 +635,6 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
|
||||
runSyncNow: async () => {
|
||||
await runSyncOnce();
|
||||
},
|
||||
getCurrentMediaId: () => activeCurrentMediaId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-
|
||||
const CHARACTER_DICTIONARY_MODAL: OverlayHostedModal = 'character-dictionary';
|
||||
const CHARACTER_DICTIONARY_OPEN_TIMEOUT_MS = 1500;
|
||||
|
||||
export async function openCharacterDictionaryModal(deps: {
|
||||
async function openCharacterDictionaryModalChannel(deps: {
|
||||
ensureOverlayStartupPrereqs: () => void;
|
||||
ensureOverlayWindowsReadyForVisibilityActions: () => void;
|
||||
sendToActiveOverlayWindow: (
|
||||
@@ -18,6 +18,8 @@ export async function openCharacterDictionaryModal(deps: {
|
||||
) => boolean;
|
||||
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
|
||||
logWarn: (message: string) => void;
|
||||
channel: string;
|
||||
retryWarning: string;
|
||||
}): Promise<boolean> {
|
||||
return await retryOverlayModalOpen(
|
||||
{
|
||||
@@ -27,8 +29,7 @@ export async function openCharacterDictionaryModal(deps: {
|
||||
{
|
||||
modal: CHARACTER_DICTIONARY_MODAL,
|
||||
timeoutMs: CHARACTER_DICTIONARY_OPEN_TIMEOUT_MS,
|
||||
retryWarning:
|
||||
'Character dictionary modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||
retryWarning: deps.retryWarning,
|
||||
sendOpen: () =>
|
||||
openOverlayHostedModal(
|
||||
{
|
||||
@@ -38,7 +39,7 @@ export async function openCharacterDictionaryModal(deps: {
|
||||
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
|
||||
},
|
||||
{
|
||||
channel: IPC_CHANNELS.event.characterDictionaryOpen,
|
||||
channel: deps.channel,
|
||||
modal: CHARACTER_DICTIONARY_MODAL,
|
||||
preferModalWindow: true,
|
||||
},
|
||||
@@ -46,3 +47,30 @@ export async function openCharacterDictionaryModal(deps: {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
type OpenCharacterDictionaryModalDeps = Omit<
|
||||
Parameters<typeof openCharacterDictionaryModalChannel>[0],
|
||||
'channel' | 'retryWarning'
|
||||
>;
|
||||
|
||||
export async function openCharacterDictionaryModal(
|
||||
deps: OpenCharacterDictionaryModalDeps,
|
||||
): Promise<boolean> {
|
||||
return await openCharacterDictionaryModalChannel({
|
||||
...deps,
|
||||
channel: IPC_CHANNELS.event.characterDictionaryOpen,
|
||||
retryWarning:
|
||||
'Character dictionary modal did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||
});
|
||||
}
|
||||
|
||||
export async function openCharacterDictionaryManagerModal(
|
||||
deps: OpenCharacterDictionaryModalDeps,
|
||||
): Promise<boolean> {
|
||||
return await openCharacterDictionaryModalChannel({
|
||||
...deps,
|
||||
channel: IPC_CHANNELS.event.characterDictionaryManagerOpen,
|
||||
retryWarning:
|
||||
'Character dictionary manager did not acknowledge modal open on first attempt; retrying dedicated modal window.',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ function createShortcuts(): ConfiguredShortcuts {
|
||||
multiCopyTimeoutMs: 5000,
|
||||
toggleSecondarySub: null,
|
||||
markAudioCard: null,
|
||||
openCharacterDictionary: null,
|
||||
openCharacterDictionaryManager: null,
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openSessionHelp: null,
|
||||
|
||||
@@ -20,7 +20,7 @@ function createShortcuts(): ConfiguredShortcuts {
|
||||
multiCopyTimeoutMs: 5000,
|
||||
toggleSecondarySub: null,
|
||||
markAudioCard: null,
|
||||
openCharacterDictionary: null,
|
||||
openCharacterDictionaryManager: null,
|
||||
openRuntimeOptions: null,
|
||||
openJimaku: null,
|
||||
openSessionHelp: null,
|
||||
|
||||
@@ -17,6 +17,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
|
||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||
openCharacterDictionary: () => calls.push('character-dictionary'),
|
||||
openCharacterDictionaryManager: () => calls.push('character-dictionary-manager'),
|
||||
openJimaku: () => calls.push('jimaku'),
|
||||
markAudioCard: async () => {
|
||||
calls.push('mark-audio');
|
||||
@@ -49,6 +50,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
|
||||
deps.showMpvOsd('x');
|
||||
deps.openRuntimeOptionsPalette();
|
||||
deps.openCharacterDictionary();
|
||||
deps.openCharacterDictionaryManager();
|
||||
deps.openJimaku();
|
||||
await deps.markAudioCard();
|
||||
deps.copySubtitleMultiple(5000);
|
||||
@@ -66,6 +68,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
|
||||
'osd:x',
|
||||
'runtime-options',
|
||||
'character-dictionary',
|
||||
'character-dictionary-manager',
|
||||
'jimaku',
|
||||
'mark-audio',
|
||||
'copy-multi:5000',
|
||||
|
||||
@@ -12,6 +12,7 @@ export function createBuildOverlayShortcutsRuntimeMainDepsHandler(
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||
openCharacterDictionary: () => deps.openCharacterDictionary(),
|
||||
openCharacterDictionaryManager: () => deps.openCharacterDictionaryManager(),
|
||||
openJimaku: () => deps.openJimaku(),
|
||||
markAudioCard: () => deps.markAudioCard(),
|
||||
copySubtitleMultiple: (timeoutMs: number) => deps.copySubtitleMultiple(timeoutMs),
|
||||
|
||||
@@ -6,6 +6,9 @@ type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
|
||||
getNameMatchEnabled?: NonNullable<TokenizerDepsRuntimeOptions['getNameMatchEnabled']>;
|
||||
getNameMatchImagesEnabled?: NonNullable<TokenizerDepsRuntimeOptions['getNameMatchImagesEnabled']>;
|
||||
getCharacterNameImage?: NonNullable<TokenizerDepsRuntimeOptions['getCharacterNameImage']>;
|
||||
getCurrentCharacterDictionaryMediaId?: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getCurrentCharacterDictionaryMediaId']
|
||||
>;
|
||||
getFrequencyDictionaryEnabled: NonNullable<
|
||||
TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
|
||||
>;
|
||||
@@ -70,6 +73,11 @@ export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
|
||||
getCharacterNameImage: (term: string) => deps.getCharacterNameImage!(term),
|
||||
}
|
||||
: {}),
|
||||
...(deps.getCurrentCharacterDictionaryMediaId
|
||||
? {
|
||||
getCurrentCharacterDictionaryMediaId: () => deps.getCurrentCharacterDictionaryMediaId!(),
|
||||
}
|
||||
: {}),
|
||||
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
|
||||
getFrequencyDictionaryMatchMode: () => deps.getFrequencyDictionaryMatchMode(),
|
||||
getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
|
||||
|
||||
@@ -66,8 +66,7 @@ test('build tray template handler wires actions and init guards', () => {
|
||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||
showTexthookerPage: () => true,
|
||||
showFirstRunSetup: () => true,
|
||||
openFirstRunSetupWindow: (force?: boolean) =>
|
||||
calls.push(force ? 'setup-forced' : 'setup'),
|
||||
openFirstRunSetupWindow: (force?: boolean) => calls.push(force ? 'setup-forced' : 'setup'),
|
||||
showWindowsMpvLauncherSetup: () => true,
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||
@@ -118,8 +117,7 @@ test('windows mpv launcher tray action force-opens completed setup', () => {
|
||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||
showTexthookerPage: () => true,
|
||||
showFirstRunSetup: () => false,
|
||||
openFirstRunSetupWindow: (force?: boolean) =>
|
||||
calls.push(force ? 'setup-forced' : 'setup'),
|
||||
openFirstRunSetupWindow: (force?: boolean) => calls.push(force ? 'setup-forced' : 'setup'),
|
||||
showWindowsMpvLauncherSetup: () => true,
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||
|
||||
@@ -28,8 +28,7 @@ test('tray main deps builders return mapped handlers', () => {
|
||||
openTexthookerInBrowser: () => calls.push('texthooker'),
|
||||
showTexthookerPage: () => true,
|
||||
showFirstRunSetup: () => true,
|
||||
openFirstRunSetupWindow: (force?: boolean) =>
|
||||
calls.push(force ? 'setup-forced' : 'setup'),
|
||||
openFirstRunSetupWindow: (force?: boolean) => calls.push(force ? 'setup-forced' : 'setup'),
|
||||
showWindowsMpvLauncherSetup: () => true,
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openConfigSettingsWindow: () => calls.push('configuration'),
|
||||
|
||||
Reference in New Issue
Block a user