Restore multi-copy digit capture and add AniList selection (#56)

This commit is contained in:
2026-04-25 21:44:55 -07:00
committed by GitHub
parent 7ac51cd5e9
commit d8934647a9
140 changed files with 4097 additions and 326 deletions

View File

@@ -459,6 +459,66 @@ test('auto sync keeps revisited media retained when a new title is added afterwa
assert.deepEqual(state.activeMediaIds, ['1 - Title 1', '4 - Title 4', '3 - Title 3']);
});
test('auto sync removes stale manual-selection media ids when applying corrected snapshot', async () => {
const userDataPath = makeTempDir();
const dictionariesDir = path.join(userDataPath, 'character-dictionaries');
fs.mkdirSync(dictionariesDir, { recursive: true });
fs.writeFileSync(
path.join(dictionariesDir, 'auto-sync-state.json'),
JSON.stringify(
{
activeMediaIds: ['10607 - Rerere no Tensai Bakabon', '130298 - The Eminence in Shadow'],
mergedRevision: 'old',
mergedDictionaryTitle: 'SubMiner Character Dictionary',
},
null,
2,
),
);
const builtMediaIds: number[][] = [];
const runtime = createCharacterDictionaryAutoSyncRuntimeService({
userDataPath,
getConfig: () => ({
enabled: true,
maxLoaded: 5,
profileScope: 'all',
}),
getOrCreateCurrentSnapshot: async () => ({
mediaId: 21355,
mediaTitle: 'Re:ZERO -Starting Life in Another World-',
entryCount: 120,
fromCache: false,
updatedAt: 99,
staleMediaIds: [10607],
}),
buildMergedDictionary: async (mediaIds) => {
builtMediaIds.push([...mediaIds]);
return {
zipPath: path.join(dictionariesDir, 'merged.zip'),
revision: `rev-${mediaIds.join('-')}`,
dictionaryTitle: 'SubMiner Character Dictionary',
entryCount: 200,
};
},
getYomitanDictionaryInfo: async () => [],
importYomitanDictionary: async () => true,
deleteYomitanDictionary: async () => true,
upsertYomitanDictionarySettings: async () => false,
now: () => 1,
});
await runtime.runSyncNow();
assert.deepEqual(builtMediaIds, [[21355, 130298]]);
const state = JSON.parse(
fs.readFileSync(path.join(dictionariesDir, 'auto-sync-state.json'), 'utf8'),
) as { activeMediaIds: string[] };
assert.deepEqual(state.activeMediaIds, [
'21355 - Re:ZERO -Starting Life in Another World-',
'130298 - The Eminence in Shadow',
]);
});
test('auto sync persists rebuilt MRU state even if Yomitan import fails afterward', async () => {
const userDataPath = makeTempDir();
const dictionariesDir = path.join(userDataPath, 'character-dictionaries');

View File

@@ -271,12 +271,19 @@ export function createCharacterDictionaryAutoSyncRuntimeService(
currentMediaId = snapshot.mediaId;
currentMediaTitle = snapshot.mediaTitle;
const state = readAutoSyncState(statePath);
const staleMediaIds = new Set(
(snapshot.staleMediaIds ?? [])
.map((mediaId) => normalizeMediaId(mediaId))
.filter((mediaId): mediaId is number => mediaId !== null),
);
const nextActiveMediaIds = [
{
mediaId: snapshot.mediaId,
label: buildActiveMediaLabel(snapshot.mediaId, snapshot.mediaTitle),
},
...state.activeMediaIds.filter((entry) => entry.mediaId !== snapshot.mediaId),
...state.activeMediaIds.filter(
(entry) => entry.mediaId !== snapshot.mediaId && !staleMediaIds.has(entry.mediaId),
),
].slice(0, Math.max(1, Math.floor(config.maxLoaded)));
const nextActiveMediaIdValues = nextActiveMediaIds.map((entry) => entry.mediaId);
deps.logInfo?.(

View File

@@ -0,0 +1,48 @@
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
import { openOverlayHostedModal, retryOverlayModalOpen } from './overlay-hosted-modal-open';
const CHARACTER_DICTIONARY_MODAL: OverlayHostedModal = 'character-dictionary';
const CHARACTER_DICTIONARY_OPEN_TIMEOUT_MS = 1500;
export async function openCharacterDictionaryModal(deps: {
ensureOverlayStartupPrereqs: () => void;
ensureOverlayWindowsReadyForVisibilityActions: () => void;
sendToActiveOverlayWindow: (
channel: string,
payload?: unknown,
runtimeOptions?: {
restoreOnModalClose?: OverlayHostedModal;
preferModalWindow?: boolean;
},
) => boolean;
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
logWarn: (message: string) => void;
}): Promise<boolean> {
return await retryOverlayModalOpen(
{
waitForModalOpen: deps.waitForModalOpen,
logWarn: deps.logWarn,
},
{
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.',
sendOpen: () =>
openOverlayHostedModal(
{
ensureOverlayStartupPrereqs: deps.ensureOverlayStartupPrereqs,
ensureOverlayWindowsReadyForVisibilityActions:
deps.ensureOverlayWindowsReadyForVisibilityActions,
sendToActiveOverlayWindow: deps.sendToActiveOverlayWindow,
},
{
channel: IPC_CHANNELS.event.characterDictionaryOpen,
modal: CHARACTER_DICTIONARY_MODAL,
preferModalWindow: true,
},
),
},
);
}

View File

@@ -19,6 +19,7 @@ test('build cli command context deps maps handlers and values', () => {
isOverlayInitialized: () => true,
initializeOverlay: () => calls.push('init'),
toggleVisibleOverlay: () => calls.push('toggle-visible'),
togglePrimarySubtitleBar: () => calls.push('toggle-primary-subtitle'),
openFirstRunSetup: () => calls.push('setup'),
setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`),
copyCurrentSubtitle: () => calls.push('copy'),

View File

@@ -17,6 +17,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
isOverlayInitialized: () => boolean;
initializeOverlay: () => void;
toggleVisibleOverlay: () => void;
togglePrimarySubtitleBar: () => void;
openFirstRunSetup: () => void;
setVisibleOverlay: (visible: boolean) => void;
copyCurrentSubtitle: () => void;
@@ -36,6 +37,8 @@ export function createBuildCliCommandContextDepsHandler(deps: {
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
getCharacterDictionarySelection?: CliCommandContextFactoryDeps['getCharacterDictionarySelection'];
setCharacterDictionarySelection?: CliCommandContextFactoryDeps['setCharacterDictionarySelection'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
@@ -67,6 +70,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
isOverlayInitialized: deps.isOverlayInitialized,
initializeOverlay: deps.initializeOverlay,
toggleVisibleOverlay: deps.toggleVisibleOverlay,
togglePrimarySubtitleBar: deps.togglePrimarySubtitleBar,
openFirstRunSetup: deps.openFirstRunSetup,
setVisibleOverlay: deps.setVisibleOverlay,
copyCurrentSubtitle: deps.copyCurrentSubtitle,
@@ -86,6 +90,8 @@ export function createBuildCliCommandContextDepsHandler(deps: {
getAnilistQueueStatus: deps.getAnilistQueueStatus,
retryAnilistQueueNow: deps.retryAnilistQueueNow,
generateCharacterDictionary: deps.generateCharacterDictionary,
getCharacterDictionarySelection: deps.getCharacterDictionarySelection,
setCharacterDictionarySelection: deps.setCharacterDictionarySelection,
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,

View File

@@ -26,6 +26,7 @@ test('cli command context factory composes main deps and context handlers', () =
showMpvOsd: (text) => calls.push(`osd:${text}`),
initializeOverlayRuntime: () => calls.push('init-overlay'),
toggleVisibleOverlay: () => calls.push('toggle-visible'),
togglePrimarySubtitleBar: () => calls.push('toggle-primary-subtitle'),
openFirstRunSetupWindow: () => calls.push('setup'),
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
copyCurrentSubtitle: () => calls.push('copy-sub'),

View File

@@ -29,6 +29,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
initializeOverlayRuntime: () => calls.push('init-overlay'),
toggleVisibleOverlay: () => calls.push('toggle-visible'),
togglePrimarySubtitleBar: () => calls.push('toggle-primary-subtitle'),
openFirstRunSetupWindow: () => calls.push('open-setup'),
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),

View File

@@ -27,6 +27,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
initializeOverlayRuntime: () => void;
toggleVisibleOverlay: () => void;
togglePrimarySubtitleBar: () => void;
openFirstRunSetupWindow: () => void;
setVisibleOverlayVisible: (visible: boolean) => void;
@@ -48,6 +49,8 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
getCharacterDictionarySelection?: CliCommandContextFactoryDeps['getCharacterDictionarySelection'];
setCharacterDictionarySelection?: CliCommandContextFactoryDeps['setCharacterDictionarySelection'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
@@ -92,6 +95,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized,
initializeOverlay: () => deps.initializeOverlayRuntime(),
toggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
togglePrimarySubtitleBar: () => deps.togglePrimarySubtitleBar(),
openFirstRunSetup: () => deps.openFirstRunSetupWindow(),
setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
copyCurrentSubtitle: () => deps.copyCurrentSubtitle(),
@@ -113,6 +117,8 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
generateCharacterDictionary: (targetPath?: string) =>
deps.generateCharacterDictionary(targetPath),
getCharacterDictionarySelection: deps.getCharacterDictionarySelection,
setCharacterDictionarySelection: deps.setCharacterDictionarySelection,
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
runYoutubePlaybackFlow: (request) => deps.runYoutubePlaybackFlow(request),

View File

@@ -25,6 +25,7 @@ function createDeps() {
isOverlayInitialized: () => true,
initializeOverlay: () => {},
toggleVisibleOverlay: () => {},
togglePrimarySubtitleBar: () => {},
openFirstRunSetup: () => {},
setVisibleOverlay: () => {},
copyCurrentSubtitle: () => {},

View File

@@ -22,6 +22,7 @@ export type CliCommandContextFactoryDeps = {
isOverlayInitialized: () => boolean;
initializeOverlay: () => void;
toggleVisibleOverlay: () => void;
togglePrimarySubtitleBar: () => void;
openFirstRunSetup: () => void;
setVisibleOverlay: (visible: boolean) => void;
copyCurrentSubtitle: () => void;
@@ -41,6 +42,8 @@ export type CliCommandContextFactoryDeps = {
getAnilistQueueStatus: CliCommandRuntimeServiceContext['getAnilistQueueStatus'];
retryAnilistQueueNow: CliCommandRuntimeServiceContext['retryAnilistQueueNow'];
generateCharacterDictionary: CliCommandRuntimeServiceContext['generateCharacterDictionary'];
getCharacterDictionarySelection?: CliCommandRuntimeServiceContext['getCharacterDictionarySelection'];
setCharacterDictionarySelection?: CliCommandRuntimeServiceContext['setCharacterDictionarySelection'];
runStatsCommand: CliCommandRuntimeServiceContext['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: CliCommandRuntimeServiceContext['runYoutubePlaybackFlow'];
@@ -79,6 +82,7 @@ export function createCliCommandContext(
isOverlayInitialized: deps.isOverlayInitialized,
initializeOverlay: deps.initializeOverlay,
toggleVisibleOverlay: deps.toggleVisibleOverlay,
togglePrimarySubtitleBar: deps.togglePrimarySubtitleBar,
openFirstRunSetup: deps.openFirstRunSetup,
setVisibleOverlay: deps.setVisibleOverlay,
copyCurrentSubtitle: deps.copyCurrentSubtitle,
@@ -98,6 +102,23 @@ export function createCliCommandContext(
getAnilistQueueStatus: deps.getAnilistQueueStatus,
retryAnilistQueueNow: deps.retryAnilistQueueNow,
generateCharacterDictionary: deps.generateCharacterDictionary,
getCharacterDictionarySelection:
deps.getCharacterDictionarySelection ??
(async () => ({
seriesKey: '',
guessTitle: null,
current: null,
override: null,
candidates: [],
})),
setCharacterDictionarySelection:
deps.setCharacterDictionarySelection ??
(async () => ({
ok: false,
seriesKey: '',
selected: { id: 0, title: '', episodes: null },
staleMediaIds: [],
})),
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,

View File

@@ -19,6 +19,7 @@ test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
showMpvOsd: () => {},
initializeOverlayRuntime: () => {},
toggleVisibleOverlay: () => {},
togglePrimarySubtitleBar: () => {},
openFirstRunSetupWindow: () => {},
setVisibleOverlayVisible: () => {},
copyCurrentSubtitle: () => {},

View File

@@ -20,12 +20,14 @@ function withTempDir(fn: (dir: string) => Promise<void> | void): Promise<void> |
function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
return {
background: false,
managedPlayback: false,
start: false,
launchMpv: false,
launchMpvTargets: [],
stop: false,
toggle: false,
toggleVisibleOverlay: false,
togglePrimarySubtitleBar: false,
settings: false,
setup: false,
show: false,
@@ -51,6 +53,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
openJimaku: false,
openYoutubePicker: false,
openPlaylistBrowser: false,
openCharacterDictionary: false,
replayCurrentSubtitle: false,
playNextSubtitle: false,
shiftSubDelayPrevLine: false,
@@ -62,6 +65,9 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
anilistSetup: false,
anilistRetryQueue: false,
dictionary: false,
dictionaryCandidates: false,
dictionarySelect: false,
dictionaryAnilistId: undefined,
stats: false,
jellyfin: false,
jellyfinLogin: false,

View File

@@ -60,6 +60,7 @@ function hasAnyStartupCommandBeyondSetup(args: CliArgs): boolean {
return Boolean(
args.toggle ||
args.toggleVisibleOverlay ||
args.togglePrimarySubtitleBar ||
args.launchMpv ||
args.settings ||
args.show ||

View File

@@ -16,6 +16,7 @@ function createShortcuts(): ConfiguredShortcuts {
multiCopyTimeoutMs: 5000,
toggleSecondarySub: null,
markAudioCard: null,
openCharacterDictionary: null,
openRuntimeOptions: null,
openJimaku: null,
openSessionHelp: null,

View File

@@ -20,6 +20,7 @@ function createShortcuts(): ConfiguredShortcuts {
multiCopyTimeoutMs: 5000,
toggleSecondarySub: null,
markAudioCard: null,
openCharacterDictionary: null,
openRuntimeOptions: null,
openJimaku: null,
openSessionHelp: null,

View File

@@ -11,6 +11,8 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
shouldQuitOnMpvShutdown?: () => boolean;
requestAppQuit?: () => void;
bindEventHandlers: (client: TClient) => void;
}) {
return () => ({
@@ -24,6 +26,8 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
getReconnectTimer: () => deps.getReconnectTimer(),
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) =>
deps.setReconnectTimer(timer),
shouldQuitOnMpvShutdown: () => deps.shouldQuitOnMpvShutdown?.() ?? false,
requestAppQuit: () => deps.requestAppQuit?.(),
},
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
});

View File

@@ -7,6 +7,8 @@ export type MpvClientRuntimeServiceOptions = {
isVisibleOverlayVisible: () => boolean;
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
shouldQuitOnMpvShutdown?: () => boolean;
requestAppQuit?: () => void;
};
type MpvClientLike = {

View File

@@ -16,6 +16,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
isOverlayShortcutContextActive: () => false,
showMpvOsd: (text) => calls.push(`osd:${text}`),
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
openCharacterDictionary: () => calls.push('character-dictionary'),
openJimaku: () => calls.push('jimaku'),
markAudioCard: async () => {
calls.push('mark-audio');
@@ -47,6 +48,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
assert.equal(shortcutsRegistered, true);
deps.showMpvOsd('x');
deps.openRuntimeOptionsPalette();
deps.openCharacterDictionary();
deps.openJimaku();
await deps.markAudioCard();
deps.copySubtitleMultiple(5000);
@@ -63,6 +65,7 @@ test('overlay shortcuts runtime main deps builder maps lifecycle and action call
'registered:true',
'osd:x',
'runtime-options',
'character-dictionary',
'jimaku',
'mark-audio',
'copy-multi:5000',

View File

@@ -11,6 +11,7 @@ export function createBuildOverlayShortcutsRuntimeMainDepsHandler(
isOverlayShortcutContextActive: () => deps.isOverlayShortcutContextActive?.() ?? true,
showMpvOsd: (text: string) => deps.showMpvOsd(text),
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
openCharacterDictionary: () => deps.openCharacterDictionary(),
openJimaku: () => deps.openJimaku(),
markAudioCard: () => deps.markAudioCard(),
copySubtitleMultiple: (timeoutMs: number) => deps.copySubtitleMultiple(timeoutMs),

View File

@@ -41,7 +41,7 @@ test('build tray template handler wires actions and init guards', () => {
let initialized = false;
const buildTemplate = createBuildTrayMenuTemplateHandler({
buildTrayMenuTemplateRuntime: (handlers) => {
handlers.openOverlay();
handlers.openSessionHelp();
handlers.openFirstRunSetup();
handlers.openWindowsMpvLauncherSetup();
handlers.openYomitanSettings();
@@ -56,7 +56,7 @@ test('build tray template handler wires actions and init guards', () => {
calls.push('init');
},
isOverlayRuntimeInitialized: () => initialized,
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
openSessionHelpModal: () => calls.push('help'),
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => calls.push('setup'),
showWindowsMpvLauncherSetup: () => true,
@@ -71,7 +71,7 @@ test('build tray template handler wires actions and init guards', () => {
assert.deepEqual(template, [{ label: 'ok' }]);
assert.deepEqual(calls, [
'init',
'visible:true',
'help',
'setup',
'setup',
'yomitan',

View File

@@ -28,7 +28,7 @@ export function createResolveTrayIconPathHandler(deps: {
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
buildTrayMenuTemplateRuntime: (handlers: {
openOverlay: () => void;
openSessionHelp: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -41,7 +41,7 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
}) => TMenuItem[];
initializeOverlayRuntime: () => void;
isOverlayRuntimeInitialized: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
openSessionHelpModal: () => void;
showFirstRunSetup: () => boolean;
openFirstRunSetupWindow: () => void;
showWindowsMpvLauncherSetup: () => boolean;
@@ -53,11 +53,11 @@ export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
}) {
return (): TMenuItem[] => {
return deps.buildTrayMenuTemplateRuntime({
openOverlay: () => {
openSessionHelp: () => {
if (!deps.isOverlayRuntimeInitialized()) {
deps.initializeOverlayRuntime();
}
deps.setVisibleOverlayVisible(true);
deps.openSessionHelpModal();
},
openFirstRunSetup: () => {
deps.openFirstRunSetupWindow();

View File

@@ -24,7 +24,7 @@ test('tray main deps builders return mapped handlers', () => {
buildTrayMenuTemplateRuntime: () => [{ label: 'tray' }] as never,
initializeOverlayRuntime: () => calls.push('init'),
isOverlayRuntimeInitialized: () => false,
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
openSessionHelpModal: () => calls.push('help'),
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => calls.push('setup'),
showWindowsMpvLauncherSetup: () => true,
@@ -36,7 +36,7 @@ test('tray main deps builders return mapped handlers', () => {
})();
const template = menuDeps.buildTrayMenuTemplateRuntime({
openOverlay: () => calls.push('open-overlay'),
openSessionHelp: () => calls.push('open-help'),
openFirstRunSetup: () => calls.push('open-setup'),
showFirstRunSetup: true,
openWindowsMpvLauncherSetup: () => calls.push('open-windows-mpv'),

View File

@@ -27,7 +27,7 @@ export function createBuildResolveTrayIconPathMainDepsHandler(deps: {
export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
buildTrayMenuTemplateRuntime: (handlers: {
openOverlay: () => void;
openSessionHelp: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -40,7 +40,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
}) => TMenuItem[];
initializeOverlayRuntime: () => void;
isOverlayRuntimeInitialized: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
openSessionHelpModal: () => void;
showFirstRunSetup: () => boolean;
openFirstRunSetupWindow: () => void;
showWindowsMpvLauncherSetup: () => boolean;
@@ -54,7 +54,7 @@ export function createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>(deps: {
buildTrayMenuTemplateRuntime: deps.buildTrayMenuTemplateRuntime,
initializeOverlayRuntime: deps.initializeOverlayRuntime,
isOverlayRuntimeInitialized: deps.isOverlayRuntimeInitialized,
setVisibleOverlayVisible: deps.setVisibleOverlayVisible,
openSessionHelpModal: deps.openSessionHelpModal,
showFirstRunSetup: deps.showFirstRunSetup,
openFirstRunSetupWindow: deps.openFirstRunSetupWindow,
showWindowsMpvLauncherSetup: deps.showWindowsMpvLauncherSetup,

View File

@@ -19,14 +19,12 @@ test('tray runtime handlers compose resolve/menu/ensure/destroy handlers', () =>
fileExists: () => true,
},
buildTrayMenuTemplateDeps: {
buildTrayMenuTemplateRuntime: () => [{ label: 'Open Overlay' }],
buildTrayMenuTemplateRuntime: () => [{ label: 'Open Help' }],
initializeOverlayRuntime: () => {
overlayInitialized = true;
},
isOverlayRuntimeInitialized: () => overlayInitialized,
setVisibleOverlayVisible: (visible) => {
visibleOverlay = visible;
},
openSessionHelpModal: () => {},
showFirstRunSetup: () => true,
openFirstRunSetupWindow: () => {},
showWindowsMpvLauncherSetup: () => true,
@@ -88,7 +86,7 @@ test('tray runtime handlers compose resolve/menu/ensure/destroy handlers', () =>
});
assert.equal(runtime.resolveTrayIconPath(), '/tmp/SubMiner.png');
assert.deepEqual(runtime.buildTrayMenu(), { template: [{ label: 'Open Overlay' }] });
assert.deepEqual(runtime.buildTrayMenu(), { template: [{ label: 'Open Help' }] });
runtime.ensureTray();
assert.equal(overlayInitialized, true);
assert.equal(visibleOverlay, true);

View File

@@ -29,7 +29,7 @@ test('resolve tray icon returns null when no asset exists', () => {
test('tray menu template contains expected entries and handlers', () => {
const calls: string[] = [];
const template = buildTrayMenuTemplateRuntime({
openOverlay: () => calls.push('overlay'),
openSessionHelp: () => calls.push('help'),
openFirstRunSetup: () => calls.push('setup'),
showFirstRunSetup: true,
openWindowsMpvLauncherSetup: () => calls.push('windows-mpv'),
@@ -42,15 +42,17 @@ test('tray menu template contains expected entries and handlers', () => {
});
assert.equal(template.length, 9);
assert.equal(template.some((entry) => entry.label === 'Open Overlay'), false);
assert.equal(template[0]!.label, 'Open Help');
template[0]!.click?.();
template[7]!.type === 'separator' ? calls.push('separator') : calls.push('bad');
template[8]!.click?.();
assert.deepEqual(calls, ['overlay', 'separator', 'quit']);
assert.deepEqual(calls, ['help', 'separator', 'quit']);
});
test('tray menu template omits first-run setup entry when setup is complete', () => {
const labels = buildTrayMenuTemplateRuntime({
openOverlay: () => undefined,
openSessionHelp: () => undefined,
openFirstRunSetup: () => undefined,
showFirstRunSetup: false,
openWindowsMpvLauncherSetup: () => undefined,

View File

@@ -30,7 +30,7 @@ export function resolveTrayIconPathRuntime(deps: {
}
export type TrayMenuActionHandlers = {
openOverlay: () => void;
openSessionHelp: () => void;
openFirstRunSetup: () => void;
showFirstRunSetup: boolean;
openWindowsMpvLauncherSetup: () => void;
@@ -49,8 +49,8 @@ export function buildTrayMenuTemplateRuntime(handlers: TrayMenuActionHandlers):
}> {
return [
{
label: 'Open Overlay',
click: handlers.openOverlay,
label: 'Open Help',
click: handlers.openSessionHelp,
},
...(handlers.showFirstRunSetup
? [