mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract main runtime lifecycle helper builders
This commit is contained in:
541
src/main.ts
541
src/main.ts
@@ -208,6 +208,37 @@ import {
|
||||
createHandleMineSentenceDigitHandler,
|
||||
createHandleMultiCopyDigitHandler,
|
||||
} from './main/runtime/mining-actions';
|
||||
import {
|
||||
createSetInvisibleOverlayVisibleHandler,
|
||||
createSetVisibleOverlayVisibleHandler,
|
||||
createToggleInvisibleOverlayHandler,
|
||||
createToggleVisibleOverlayHandler,
|
||||
} from './main/runtime/overlay-visibility-actions';
|
||||
import {
|
||||
createAppendClipboardVideoToQueueHandler,
|
||||
createHandleOverlayModalClosedHandler,
|
||||
createSetOverlayVisibleHandler,
|
||||
createToggleOverlayHandler,
|
||||
} from './main/runtime/overlay-main-actions';
|
||||
import {
|
||||
createHandleMpvCommandFromIpcHandler,
|
||||
createRunSubsyncManualFromIpcHandler,
|
||||
} from './main/runtime/ipc-bridge-actions';
|
||||
import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
} from './main/runtime/overlay-window-factory';
|
||||
import {
|
||||
createBuildTrayMenuTemplateHandler,
|
||||
createResolveTrayIconPathHandler,
|
||||
} from './main/runtime/tray-main-actions';
|
||||
import {
|
||||
createEnsureYomitanExtensionLoadedHandler,
|
||||
createLoadYomitanExtensionHandler,
|
||||
} from './main/runtime/yomitan-extension-loader';
|
||||
import { createBuildInitializeOverlayRuntimeOptionsHandler } from './main/runtime/overlay-runtime-options';
|
||||
import { createBuildCliCommandContextDepsHandler } from './main/runtime/cli-command-context-deps';
|
||||
import {
|
||||
buildRestartRequiredConfigMessage,
|
||||
createConfigHotReloadAppliedHandler,
|
||||
@@ -1926,57 +1957,7 @@ function handleCliCommand(args: CliArgs, source: CliCommandSource = 'initial'):
|
||||
logInfo: (message) => logger.info(message),
|
||||
})(args);
|
||||
|
||||
const cliContext = createCliCommandContext({
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
showOsd: (text: string) => showMpvOsd(text),
|
||||
texthookerService,
|
||||
getTexthookerPort: () => appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
appState.texthookerPort = port;
|
||||
},
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openExternal: (url: string) => shell.openExternal(url),
|
||||
logBrowserOpenError: (url: string, error: unknown) =>
|
||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
||||
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlay: () => initializeOverlayRuntime(),
|
||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||
setVisibleOverlay: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlay: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||
refreshKnownWordCache: () => refreshKnownWordCache(),
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
||||
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
stopApp: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
logWarn: (message: string) => logger.warn(message),
|
||||
logError: (message: string, err: unknown) => logger.error(message, err),
|
||||
});
|
||||
const cliContext = createCliCommandContext(buildCliCommandContextDepsHandler());
|
||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext);
|
||||
}
|
||||
|
||||
@@ -2213,112 +2194,30 @@ const enforceOverlayLayerOrder = createEnforceOverlayLayerOrderHandler({
|
||||
});
|
||||
|
||||
async function loadYomitanExtension(): Promise<Extension | null> {
|
||||
return loadYomitanExtensionCore({
|
||||
userDataPath: USER_DATA_PATH,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window;
|
||||
},
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
},
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
},
|
||||
setYomitanExtension: (extension) => {
|
||||
appState.yomitanExt = extension;
|
||||
},
|
||||
});
|
||||
return loadYomitanExtensionHandler();
|
||||
}
|
||||
|
||||
async function ensureYomitanExtensionLoaded(): Promise<Extension | null> {
|
||||
if (appState.yomitanExt) {
|
||||
return appState.yomitanExt;
|
||||
}
|
||||
if (yomitanLoadInFlight) {
|
||||
return yomitanLoadInFlight;
|
||||
}
|
||||
|
||||
yomitanLoadInFlight = loadYomitanExtension().finally(() => {
|
||||
yomitanLoadInFlight = null;
|
||||
});
|
||||
return yomitanLoadInFlight;
|
||||
return ensureYomitanExtensionLoadedHandler();
|
||||
}
|
||||
|
||||
function createOverlayWindow(kind: 'visible' | 'invisible'): BrowserWindow {
|
||||
return createOverlayWindowCore(kind, {
|
||||
isDev,
|
||||
overlayDebugVisualizationEnabled: appState.overlayDebugVisualizationEnabled,
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||
isOverlayVisible: (windowKind) =>
|
||||
windowKind === 'visible'
|
||||
? overlayManager.getVisibleOverlayVisible()
|
||||
: overlayManager.getInvisibleOverlayVisible(),
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
if (windowKind === 'visible') {
|
||||
overlayManager.setMainWindow(null);
|
||||
} else {
|
||||
overlayManager.setInvisibleWindow(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
return createOverlayWindowHandler(kind);
|
||||
}
|
||||
|
||||
function createMainWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow('visible');
|
||||
overlayManager.setMainWindow(window);
|
||||
return window;
|
||||
return createMainWindowHandler();
|
||||
}
|
||||
function createInvisibleWindow(): BrowserWindow {
|
||||
const window = createOverlayWindow('invisible');
|
||||
overlayManager.setInvisibleWindow(window);
|
||||
return window;
|
||||
return createInvisibleWindowHandler();
|
||||
}
|
||||
|
||||
function resolveTrayIconPath(): string | null {
|
||||
return resolveTrayIconPathRuntime({
|
||||
platform: process.platform,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
dirname: __dirname,
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
fileExists: (candidate) => fs.existsSync(candidate),
|
||||
});
|
||||
return resolveTrayIconPathHandler();
|
||||
}
|
||||
|
||||
function buildTrayMenu(): Menu {
|
||||
return Menu.buildFromTemplate(
|
||||
buildTrayMenuTemplateRuntime({
|
||||
openOverlay: () => {
|
||||
if (!appState.overlayRuntimeInitialized) {
|
||||
initializeOverlayRuntime();
|
||||
}
|
||||
setVisibleOverlayVisible(true);
|
||||
},
|
||||
openYomitanSettings: () => {
|
||||
openYomitanSettings();
|
||||
},
|
||||
openRuntimeOptions: () => {
|
||||
if (!appState.overlayRuntimeInitialized) {
|
||||
initializeOverlayRuntime();
|
||||
}
|
||||
openRuntimeOptionsPalette();
|
||||
},
|
||||
openJellyfinSetup: () => {
|
||||
openJellyfinSetupWindow();
|
||||
},
|
||||
openAnilistSetup: () => {
|
||||
openAnilistSetupWindow();
|
||||
},
|
||||
quitApp: () => {
|
||||
app.quit();
|
||||
},
|
||||
}),
|
||||
);
|
||||
return Menu.buildFromTemplate(buildTrayMenuTemplateHandler());
|
||||
}
|
||||
|
||||
function ensureTray(): void {
|
||||
@@ -2357,50 +2256,7 @@ function initializeOverlayRuntime(): void {
|
||||
createInitializeOverlayRuntimeHandler({
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => ({
|
||||
backendOverride: appState.backendOverride,
|
||||
getInitialInvisibleOverlayVisibility: () =>
|
||||
configDerivedRuntime.getInitialInvisibleOverlayVisibility(),
|
||||
createMainWindow: () => {
|
||||
createMainWindow();
|
||||
},
|
||||
createInvisibleWindow: () => {
|
||||
createInvisibleWindow();
|
||||
},
|
||||
registerGlobalShortcuts: () => {
|
||||
registerGlobalShortcuts();
|
||||
},
|
||||
updateVisibleOverlayBounds: (geometry) => {
|
||||
updateVisibleOverlayBounds(geometry);
|
||||
},
|
||||
updateInvisibleOverlayBounds: (geometry) => {
|
||||
updateInvisibleOverlayBounds(geometry);
|
||||
},
|
||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => {
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
},
|
||||
updateInvisibleOverlayVisibility: () => {
|
||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility();
|
||||
},
|
||||
getOverlayWindows: () => getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker) => {
|
||||
appState.windowTracker = tracker;
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
appState.ankiIntegration = integration as AnkiIntegration | null;
|
||||
},
|
||||
showDesktopNotification,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback(),
|
||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||
}),
|
||||
buildOptions: () => buildInitializeOverlayRuntimeOptionsHandler(),
|
||||
setInvisibleOverlayVisible: (visible) => {
|
||||
overlayManager.setInvisibleOverlayVisible(visible);
|
||||
},
|
||||
@@ -2627,6 +2483,252 @@ const handleMineSentenceDigitHandler = createHandleMineSentenceDigitHandler({
|
||||
},
|
||||
handleMineSentenceDigitCore,
|
||||
});
|
||||
const setVisibleOverlayVisibleHandler = createSetVisibleOverlayVisibleHandler({
|
||||
setVisibleOverlayVisibleCore,
|
||||
setVisibleOverlayVisibleState: (nextVisible) => {
|
||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible) => {
|
||||
setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
|
||||
},
|
||||
});
|
||||
const setInvisibleOverlayVisibleHandler = createSetInvisibleOverlayVisibleHandler({
|
||||
setInvisibleOverlayVisibleCore,
|
||||
setInvisibleOverlayVisibleState: (nextVisible) => {
|
||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateInvisibleOverlayVisibility: () => overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||
});
|
||||
const toggleVisibleOverlayHandler = createToggleVisibleOverlayHandler({
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
});
|
||||
const toggleInvisibleOverlayHandler = createToggleInvisibleOverlayHandler({
|
||||
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
setInvisibleOverlayVisible: (visible) => setInvisibleOverlayVisible(visible),
|
||||
});
|
||||
const setOverlayVisibleHandler = createSetOverlayVisibleHandler({
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
});
|
||||
const toggleOverlayHandler = createToggleOverlayHandler({
|
||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
});
|
||||
const handleOverlayModalClosedHandler = createHandleOverlayModalClosedHandler({
|
||||
handleOverlayModalClosedRuntime: (modal) => overlayModalRuntime.handleOverlayModalClosed(modal),
|
||||
});
|
||||
const appendClipboardVideoToQueueHandler = createAppendClipboardVideoToQueueHandler({
|
||||
appendClipboardVideoToQueueRuntime,
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
readClipboardText: () => clipboard.readText(),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
sendMpvCommand: (command) => {
|
||||
sendMpvCommandRuntime(appState.mpvClient, command);
|
||||
},
|
||||
});
|
||||
const handleMpvCommandFromIpcHandler = createHandleMpvCommandFromIpcHandler({
|
||||
handleMpvCommandFromIpcRuntime,
|
||||
buildMpvCommandDeps: () => ({
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
cycleRuntimeOption: (id, direction) => {
|
||||
if (!appState.runtimeOptionsManager) {
|
||||
return { ok: false, error: 'Runtime options manager unavailable' };
|
||||
}
|
||||
return applyRuntimeOptionResultRuntime(
|
||||
appState.runtimeOptionsManager.cycleOption(id, direction),
|
||||
(text) => showMpvOsd(text),
|
||||
);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
||||
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
||||
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
||||
}),
|
||||
});
|
||||
const runSubsyncManualFromIpcHandler = createRunSubsyncManualFromIpcHandler({
|
||||
runManualFromIpc: (request: SubsyncManualRunRequest) => subsyncRuntime.runManualFromIpc(request),
|
||||
});
|
||||
const buildCliCommandContextDepsHandler = createBuildCliCommandContextDepsHandler({
|
||||
getSocketPath: () => appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
appState.mpvSocketPath = socketPath;
|
||||
},
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
showOsd: (text: string) => showMpvOsd(text),
|
||||
texthookerService,
|
||||
getTexthookerPort: () => appState.texthookerPort,
|
||||
setTexthookerPort: (port: number) => {
|
||||
appState.texthookerPort = port;
|
||||
},
|
||||
shouldOpenBrowser: () => getResolvedConfig().texthooker?.openBrowser !== false,
|
||||
openExternal: (url: string) => shell.openExternal(url),
|
||||
logBrowserOpenError: (url: string, error: unknown) =>
|
||||
logger.error(`Failed to open browser for texthooker URL: ${url}`, error),
|
||||
isOverlayInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlay: () => initializeOverlayRuntime(),
|
||||
toggleVisibleOverlay: () => toggleVisibleOverlay(),
|
||||
toggleInvisibleOverlay: () => toggleInvisibleOverlay(),
|
||||
setVisibleOverlay: (visible: boolean) => setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlay: (visible: boolean) => setInvisibleOverlayVisible(visible),
|
||||
copyCurrentSubtitle: () => copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => mineSentenceCard(),
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) =>
|
||||
startPendingMineSentenceMultiple(timeoutMs),
|
||||
updateLastCardFromClipboard: () => updateLastCardFromClipboard(),
|
||||
refreshKnownWordCache: () => refreshKnownWordCache(),
|
||||
triggerFieldGrouping: () => triggerFieldGrouping(),
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
markLastCardAsAudioCard: () => markLastCardAsAudioCard(),
|
||||
getAnilistStatus: () => anilistStateRuntime.getStatusSnapshot(),
|
||||
clearAnilistToken: () => anilistStateRuntime.clearTokenState(),
|
||||
openAnilistSetup: () => openAnilistSetupWindow(),
|
||||
openJellyfinSetup: () => openJellyfinSetupWindow(),
|
||||
getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(),
|
||||
retryAnilistQueueNow: () => processNextAnilistRetryUpdate(),
|
||||
runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
cycleSecondarySubMode: () => cycleSecondarySubMode(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
stopApp: () => app.quit(),
|
||||
hasMainWindow: () => Boolean(overlayManager.getMainWindow()),
|
||||
getMultiCopyTimeoutMs: () => getConfiguredShortcuts().multiCopyTimeoutMs,
|
||||
schedule: (fn: () => void, delayMs: number) => setTimeout(fn, delayMs),
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
logWarn: (message: string) => logger.warn(message),
|
||||
logError: (message: string, err: unknown) => logger.error(message, err),
|
||||
});
|
||||
const createOverlayWindowHandler = createCreateOverlayWindowHandler<BrowserWindow>({
|
||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||
isDev,
|
||||
getOverlayDebugVisualizationEnabled: () => appState.overlayDebugVisualizationEnabled,
|
||||
ensureOverlayWindowLevel: (window) => ensureOverlayWindowLevel(window),
|
||||
onRuntimeOptionsChanged: () => broadcastRuntimeOptionsChanged(),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) => setOverlayDebugVisualizationEnabled(enabled),
|
||||
isOverlayVisible: (windowKind) =>
|
||||
windowKind === 'visible'
|
||||
? overlayManager.getVisibleOverlayVisible()
|
||||
: overlayManager.getInvisibleOverlayVisible(),
|
||||
tryHandleOverlayShortcutLocalFallback: (input) =>
|
||||
overlayShortcutsRuntime.tryHandleOverlayShortcutLocalFallback(input),
|
||||
onWindowClosed: (windowKind) => {
|
||||
if (windowKind === 'visible') {
|
||||
overlayManager.setMainWindow(null);
|
||||
} else {
|
||||
overlayManager.setInvisibleWindow(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
const createMainWindowHandler = createCreateMainWindowHandler<BrowserWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setMainWindow: (window) => overlayManager.setMainWindow(window),
|
||||
});
|
||||
const createInvisibleWindowHandler = createCreateInvisibleWindowHandler<BrowserWindow>({
|
||||
createOverlayWindow: (kind) => createOverlayWindow(kind),
|
||||
setInvisibleWindow: (window) => overlayManager.setInvisibleWindow(window),
|
||||
});
|
||||
const resolveTrayIconPathHandler = createResolveTrayIconPathHandler({
|
||||
resolveTrayIconPathRuntime,
|
||||
platform: process.platform,
|
||||
resourcesPath: process.resourcesPath,
|
||||
appPath: app.getAppPath(),
|
||||
dirname: __dirname,
|
||||
joinPath: (...parts) => path.join(...parts),
|
||||
fileExists: (candidate) => fs.existsSync(candidate),
|
||||
});
|
||||
const buildTrayMenuTemplateHandler = createBuildTrayMenuTemplateHandler({
|
||||
buildTrayMenuTemplateRuntime,
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
setVisibleOverlayVisible: (visible) => setVisibleOverlayVisible(visible),
|
||||
openYomitanSettings: () => openYomitanSettings(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
openJellyfinSetupWindow: () => openJellyfinSetupWindow(),
|
||||
openAnilistSetupWindow: () => openAnilistSetupWindow(),
|
||||
quitApp: () => app.quit(),
|
||||
});
|
||||
const loadYomitanExtensionHandler = createLoadYomitanExtensionHandler({
|
||||
loadYomitanExtensionCore,
|
||||
userDataPath: USER_DATA_PATH,
|
||||
getYomitanParserWindow: () => appState.yomitanParserWindow,
|
||||
setYomitanParserWindow: (window) => {
|
||||
appState.yomitanParserWindow = window as BrowserWindow | null;
|
||||
},
|
||||
setYomitanParserReadyPromise: (promise) => {
|
||||
appState.yomitanParserReadyPromise = promise;
|
||||
},
|
||||
setYomitanParserInitPromise: (promise) => {
|
||||
appState.yomitanParserInitPromise = promise;
|
||||
},
|
||||
setYomitanExtension: (extension) => {
|
||||
appState.yomitanExt = extension;
|
||||
},
|
||||
});
|
||||
const ensureYomitanExtensionLoadedHandler = createEnsureYomitanExtensionLoadedHandler({
|
||||
getYomitanExtension: () => appState.yomitanExt,
|
||||
getLoadInFlight: () => yomitanLoadInFlight,
|
||||
setLoadInFlight: (promise) => {
|
||||
yomitanLoadInFlight = promise;
|
||||
},
|
||||
loadYomitanExtension: () => loadYomitanExtension(),
|
||||
});
|
||||
const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler({
|
||||
getBackendOverride: () => appState.backendOverride,
|
||||
getInitialInvisibleOverlayVisibility: () =>
|
||||
configDerivedRuntime.getInitialInvisibleOverlayVisibility(),
|
||||
createMainWindow: () => {
|
||||
createMainWindow();
|
||||
},
|
||||
createInvisibleWindow: () => {
|
||||
createInvisibleWindow();
|
||||
},
|
||||
registerGlobalShortcuts: () => {
|
||||
registerGlobalShortcuts();
|
||||
},
|
||||
updateVisibleOverlayBounds: (geometry) => {
|
||||
updateVisibleOverlayBounds(geometry);
|
||||
},
|
||||
updateInvisibleOverlayBounds: (geometry) => {
|
||||
updateInvisibleOverlayBounds(geometry);
|
||||
},
|
||||
isVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
isInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
|
||||
updateVisibleOverlayVisibility: () => {
|
||||
overlayVisibilityRuntime.updateVisibleOverlayVisibility();
|
||||
},
|
||||
updateInvisibleOverlayVisibility: () => {
|
||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility();
|
||||
},
|
||||
getOverlayWindows: () => getOverlayWindows(),
|
||||
syncOverlayShortcuts: () => overlayShortcutsRuntime.syncOverlayShortcuts(),
|
||||
setWindowTracker: (tracker) => {
|
||||
appState.windowTracker = tracker as never;
|
||||
},
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getSubtitleTimingTracker: () => appState.subtitleTimingTracker,
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
getMpvSocketPath: () => appState.mpvSocketPath,
|
||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||
setAnkiIntegration: (integration) => {
|
||||
appState.ankiIntegration = integration as AnkiIntegration | null;
|
||||
},
|
||||
showDesktopNotification,
|
||||
createFieldGroupingCallback: () => createFieldGroupingCallback() as never,
|
||||
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
|
||||
});
|
||||
|
||||
async function updateLastCardFromClipboard(): Promise<void> {
|
||||
await updateLastCardFromClipboardHandler();
|
||||
@@ -2676,90 +2778,39 @@ function refreshOverlayShortcuts(): void {
|
||||
}
|
||||
|
||||
function setVisibleOverlayVisible(visible: boolean): void {
|
||||
setVisibleOverlayVisibleCore({
|
||||
visible,
|
||||
setVisibleOverlayVisibleState: (nextVisible) => {
|
||||
overlayManager.setVisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateVisibleOverlayVisibility: () => overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () =>
|
||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
configDerivedRuntime.shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||
setMpvSubVisibility: (mpvSubVisible) => {
|
||||
setMpvSubVisibilityRuntime(appState.mpvClient, mpvSubVisible);
|
||||
},
|
||||
});
|
||||
setVisibleOverlayVisibleHandler(visible);
|
||||
}
|
||||
|
||||
function setInvisibleOverlayVisible(visible: boolean): void {
|
||||
setInvisibleOverlayVisibleCore({
|
||||
visible,
|
||||
setInvisibleOverlayVisibleState: (nextVisible) => {
|
||||
overlayManager.setInvisibleOverlayVisible(nextVisible);
|
||||
},
|
||||
updateInvisibleOverlayVisibility: () =>
|
||||
overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
|
||||
syncInvisibleOverlayMousePassthrough: () =>
|
||||
overlayVisibilityRuntime.syncInvisibleOverlayMousePassthrough(),
|
||||
});
|
||||
setInvisibleOverlayVisibleHandler(visible);
|
||||
}
|
||||
|
||||
function toggleVisibleOverlay(): void {
|
||||
setVisibleOverlayVisible(!overlayManager.getVisibleOverlayVisible());
|
||||
toggleVisibleOverlayHandler();
|
||||
}
|
||||
function toggleInvisibleOverlay(): void {
|
||||
setInvisibleOverlayVisible(!overlayManager.getInvisibleOverlayVisible());
|
||||
toggleInvisibleOverlayHandler();
|
||||
}
|
||||
function setOverlayVisible(visible: boolean): void {
|
||||
setVisibleOverlayVisible(visible);
|
||||
setOverlayVisibleHandler(visible);
|
||||
}
|
||||
function toggleOverlay(): void {
|
||||
toggleVisibleOverlay();
|
||||
toggleOverlayHandler();
|
||||
}
|
||||
function handleOverlayModalClosed(modal: OverlayHostedModal): void {
|
||||
overlayModalRuntime.handleOverlayModalClosed(modal);
|
||||
handleOverlayModalClosedHandler(modal);
|
||||
}
|
||||
|
||||
function handleMpvCommandFromIpc(command: (string | number)[]): void {
|
||||
handleMpvCommandFromIpcRuntime(command, {
|
||||
triggerSubsyncFromConfig: () => triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => openRuntimeOptionsPalette(),
|
||||
cycleRuntimeOption: (id, direction) => {
|
||||
if (!appState.runtimeOptionsManager) {
|
||||
return { ok: false, error: 'Runtime options manager unavailable' };
|
||||
}
|
||||
return applyRuntimeOptionResultRuntime(
|
||||
appState.runtimeOptionsManager.cycleOption(id, direction),
|
||||
(text) => showMpvOsd(text),
|
||||
);
|
||||
},
|
||||
showMpvOsd: (text: string) => showMpvOsd(text),
|
||||
replayCurrentSubtitle: () => replayCurrentSubtitleRuntime(appState.mpvClient),
|
||||
playNextSubtitle: () => playNextSubtitleRuntime(appState.mpvClient),
|
||||
sendMpvCommand: (rawCommand: (string | number)[]) =>
|
||||
sendMpvCommandRuntime(appState.mpvClient, rawCommand),
|
||||
isMpvConnected: () => Boolean(appState.mpvClient && appState.mpvClient.connected),
|
||||
hasRuntimeOptionsManager: () => appState.runtimeOptionsManager !== null,
|
||||
});
|
||||
handleMpvCommandFromIpcHandler(command);
|
||||
}
|
||||
|
||||
async function runSubsyncManualFromIpc(request: SubsyncManualRunRequest): Promise<SubsyncResult> {
|
||||
return subsyncRuntime.runManualFromIpc(request);
|
||||
return runSubsyncManualFromIpcHandler(request);
|
||||
}
|
||||
|
||||
function appendClipboardVideoToQueue(): { ok: boolean; message: string } {
|
||||
return appendClipboardVideoToQueueRuntime({
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
readClipboardText: () => clipboard.readText(),
|
||||
showMpvOsd: (text) => showMpvOsd(text),
|
||||
sendMpvCommand: (command) => {
|
||||
sendMpvCommandRuntime(appState.mpvClient, command);
|
||||
},
|
||||
});
|
||||
return appendClipboardVideoToQueueHandler();
|
||||
}
|
||||
|
||||
registerIpcRuntimeServices({
|
||||
|
||||
83
src/main/runtime/cli-command-context-deps.test.ts
Normal file
83
src/main/runtime/cli-command-context-deps.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
||||
|
||||
test('build cli command context deps maps handlers and values', () => {
|
||||
const calls: string[] = [];
|
||||
const buildDeps = createBuildCliCommandContextDepsHandler({
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
setSocketPath: (socketPath) => calls.push(`socket:${socketPath}`),
|
||||
getMpvClient: () => null,
|
||||
showOsd: (text) => calls.push(`osd:${text}`),
|
||||
texthookerService: { start: () => null, status: () => ({ running: false }) } as never,
|
||||
getTexthookerPort: () => 5174,
|
||||
setTexthookerPort: (port) => calls.push(`port:${port}`),
|
||||
shouldOpenBrowser: () => true,
|
||||
openExternal: async (url) => calls.push(`open:${url}`),
|
||||
logBrowserOpenError: (url) => calls.push(`open-error:${url}`),
|
||||
isOverlayInitialized: () => true,
|
||||
initializeOverlay: () => calls.push('init'),
|
||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
setVisibleOverlay: (visible) => calls.push(`set-visible:${visible}`),
|
||||
setInvisibleOverlay: (visible) => calls.push(`set-invisible:${visible}`),
|
||||
copyCurrentSubtitle: () => calls.push('copy'),
|
||||
startPendingMultiCopy: (ms) => calls.push(`multi:${ms}`),
|
||||
mineSentenceCard: async () => {
|
||||
calls.push('mine');
|
||||
},
|
||||
startPendingMineSentenceMultiple: (ms) => calls.push(`mine-multi:${ms}`),
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push('update');
|
||||
},
|
||||
refreshKnownWordCache: async () => {
|
||||
calls.push('refresh');
|
||||
},
|
||||
triggerFieldGrouping: async () => {
|
||||
calls.push('group');
|
||||
},
|
||||
triggerSubsyncFromConfig: async () => {
|
||||
calls.push('subsync');
|
||||
},
|
||||
markLastCardAsAudioCard: async () => {
|
||||
calls.push('mark');
|
||||
},
|
||||
getAnilistStatus: () => ({}) as never,
|
||||
clearAnilistToken: () => calls.push('clear-token'),
|
||||
openAnilistSetup: () => calls.push('anilist'),
|
||||
openJellyfinSetup: () => calls.push('jellyfin'),
|
||||
getAnilistQueueStatus: () => ({}) as never,
|
||||
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {
|
||||
calls.push('run-jellyfin');
|
||||
},
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
cycleSecondarySubMode: () => calls.push('cycle-secondary'),
|
||||
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||
printHelp: () => calls.push('help'),
|
||||
stopApp: () => calls.push('stop'),
|
||||
hasMainWindow: () => true,
|
||||
getMultiCopyTimeoutMs: () => 5000,
|
||||
schedule: (fn) => {
|
||||
fn();
|
||||
return setTimeout(() => {}, 0);
|
||||
},
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
logWarn: (message) => calls.push(`warn:${message}`),
|
||||
logError: (message) => calls.push(`error:${message}`),
|
||||
});
|
||||
|
||||
const deps = buildDeps();
|
||||
assert.equal(deps.getSocketPath(), '/tmp/mpv.sock');
|
||||
assert.equal(deps.getTexthookerPort(), 5174);
|
||||
assert.equal(deps.shouldOpenBrowser(), true);
|
||||
assert.equal(deps.isOverlayInitialized(), true);
|
||||
assert.equal(deps.hasMainWindow(), true);
|
||||
assert.equal(deps.getMultiCopyTimeoutMs(), 5000);
|
||||
|
||||
deps.setSocketPath('/tmp/next.sock');
|
||||
deps.showOsd('hello');
|
||||
deps.setTexthookerPort(5175);
|
||||
deps.printHelp();
|
||||
assert.deepEqual(calls, ['socket:/tmp/next.sock', 'osd:hello', 'port:5175', 'help']);
|
||||
});
|
||||
94
src/main/runtime/cli-command-context-deps.ts
Normal file
94
src/main/runtime/cli-command-context-deps.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { CliArgs } from '../../cli/args';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
||||
showOsd: (text: string) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getTexthookerPort: () => number;
|
||||
setTexthookerPort: (port: number) => void;
|
||||
shouldOpenBrowser: () => boolean;
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
logBrowserOpenError: (url: string, error: unknown) => void;
|
||||
isOverlayInitialized: () => boolean;
|
||||
initializeOverlay: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
toggleInvisibleOverlay: () => void;
|
||||
setVisibleOverlay: (visible: boolean) => void;
|
||||
setInvisibleOverlay: (visible: boolean) => void;
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWordCache: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
|
||||
clearAnilistToken: CliCommandContextFactoryDeps['clearAnilistToken'];
|
||||
openAnilistSetup: CliCommandContextFactoryDeps['openAnilistSetup'];
|
||||
openJellyfinSetup: CliCommandContextFactoryDeps['openJellyfinSetup'];
|
||||
getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
|
||||
retryAnilistQueueNow: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
|
||||
runJellyfinCommand: (args: CliArgs) => Promise<void>;
|
||||
openYomitanSettings: () => void;
|
||||
cycleSecondarySubMode: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
printHelp: () => void;
|
||||
stopApp: () => void;
|
||||
hasMainWindow: () => boolean;
|
||||
getMultiCopyTimeoutMs: () => number;
|
||||
schedule: (fn: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
|
||||
logInfo: (message: string) => void;
|
||||
logWarn: (message: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
getSocketPath: deps.getSocketPath,
|
||||
setSocketPath: deps.setSocketPath,
|
||||
getMpvClient: deps.getMpvClient,
|
||||
showOsd: deps.showOsd,
|
||||
texthookerService: deps.texthookerService,
|
||||
getTexthookerPort: deps.getTexthookerPort,
|
||||
setTexthookerPort: deps.setTexthookerPort,
|
||||
shouldOpenBrowser: deps.shouldOpenBrowser,
|
||||
openExternal: deps.openExternal,
|
||||
logBrowserOpenError: deps.logBrowserOpenError,
|
||||
isOverlayInitialized: deps.isOverlayInitialized,
|
||||
initializeOverlay: deps.initializeOverlay,
|
||||
toggleVisibleOverlay: deps.toggleVisibleOverlay,
|
||||
toggleInvisibleOverlay: deps.toggleInvisibleOverlay,
|
||||
setVisibleOverlay: deps.setVisibleOverlay,
|
||||
setInvisibleOverlay: deps.setInvisibleOverlay,
|
||||
copyCurrentSubtitle: deps.copyCurrentSubtitle,
|
||||
startPendingMultiCopy: deps.startPendingMultiCopy,
|
||||
mineSentenceCard: deps.mineSentenceCard,
|
||||
startPendingMineSentenceMultiple: deps.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: deps.updateLastCardFromClipboard,
|
||||
refreshKnownWordCache: deps.refreshKnownWordCache,
|
||||
triggerFieldGrouping: deps.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: deps.triggerSubsyncFromConfig,
|
||||
markLastCardAsAudioCard: deps.markLastCardAsAudioCard,
|
||||
getAnilistStatus: deps.getAnilistStatus,
|
||||
clearAnilistToken: deps.clearAnilistToken,
|
||||
openAnilistSetup: deps.openAnilistSetup,
|
||||
openJellyfinSetup: deps.openJellyfinSetup,
|
||||
getAnilistQueueStatus: deps.getAnilistQueueStatus,
|
||||
retryAnilistQueueNow: deps.retryAnilistQueueNow,
|
||||
runJellyfinCommand: deps.runJellyfinCommand,
|
||||
openYomitanSettings: deps.openYomitanSettings,
|
||||
cycleSecondarySubMode: deps.cycleSecondarySubMode,
|
||||
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,
|
||||
printHelp: deps.printHelp,
|
||||
stopApp: deps.stopApp,
|
||||
hasMainWindow: deps.hasMainWindow,
|
||||
getMultiCopyTimeoutMs: deps.getMultiCopyTimeoutMs,
|
||||
schedule: deps.schedule,
|
||||
logInfo: deps.logInfo,
|
||||
logWarn: deps.logWarn,
|
||||
logError: deps.logError,
|
||||
});
|
||||
}
|
||||
45
src/main/runtime/ipc-bridge-actions.test.ts
Normal file
45
src/main/runtime/ipc-bridge-actions.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createHandleMpvCommandFromIpcHandler,
|
||||
createRunSubsyncManualFromIpcHandler,
|
||||
} from './ipc-bridge-actions';
|
||||
|
||||
test('handle mpv command handler forwards command and built deps', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = {
|
||||
triggerSubsyncFromConfig: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
cycleRuntimeOption: () => ({ ok: false as const, error: 'x' }),
|
||||
showMpvOsd: () => {},
|
||||
replayCurrentSubtitle: () => {},
|
||||
playNextSubtitle: () => {},
|
||||
sendMpvCommand: () => {},
|
||||
isMpvConnected: () => true,
|
||||
hasRuntimeOptionsManager: () => true,
|
||||
};
|
||||
const handle = createHandleMpvCommandFromIpcHandler({
|
||||
handleMpvCommandFromIpcRuntime: (command, nextDeps) => {
|
||||
calls.push(`command:${command.join(':')}`);
|
||||
assert.equal(nextDeps, deps);
|
||||
},
|
||||
buildMpvCommandDeps: () => deps,
|
||||
});
|
||||
|
||||
handle(['show-text', 'hello']);
|
||||
assert.deepEqual(calls, ['command:show-text:hello']);
|
||||
});
|
||||
|
||||
test('run subsync manual handler forwards request and result', async () => {
|
||||
const calls: string[] = [];
|
||||
const run = createRunSubsyncManualFromIpcHandler({
|
||||
runManualFromIpc: async (request: { id: string }) => {
|
||||
calls.push(`request:${request.id}`);
|
||||
return { ok: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
const result = await run({ id: 'job-1' });
|
||||
assert.deepEqual(result, { ok: true });
|
||||
assert.deepEqual(calls, ['request:job-1']);
|
||||
});
|
||||
21
src/main/runtime/ipc-bridge-actions.ts
Normal file
21
src/main/runtime/ipc-bridge-actions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
|
||||
|
||||
export function createHandleMpvCommandFromIpcHandler(deps: {
|
||||
handleMpvCommandFromIpcRuntime: (
|
||||
command: (string | number)[],
|
||||
options: MpvCommandFromIpcRuntimeDeps,
|
||||
) => void;
|
||||
buildMpvCommandDeps: () => MpvCommandFromIpcRuntimeDeps;
|
||||
}) {
|
||||
return (command: (string | number)[]): void => {
|
||||
deps.handleMpvCommandFromIpcRuntime(command, deps.buildMpvCommandDeps());
|
||||
};
|
||||
}
|
||||
|
||||
export function createRunSubsyncManualFromIpcHandler<TRequest, TResult>(deps: {
|
||||
runManualFromIpc: (request: TRequest) => Promise<TResult>;
|
||||
}) {
|
||||
return async (request: TRequest): Promise<TResult> => {
|
||||
return deps.runManualFromIpc(request);
|
||||
};
|
||||
}
|
||||
60
src/main/runtime/overlay-main-actions.test.ts
Normal file
60
src/main/runtime/overlay-main-actions.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createAppendClipboardVideoToQueueHandler,
|
||||
createHandleOverlayModalClosedHandler,
|
||||
createSetOverlayVisibleHandler,
|
||||
createToggleOverlayHandler,
|
||||
} from './overlay-main-actions';
|
||||
|
||||
test('set overlay visible handler delegates to visible overlay setter', () => {
|
||||
const calls: string[] = [];
|
||||
const setOverlayVisible = createSetOverlayVisibleHandler({
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`set:${visible}`),
|
||||
});
|
||||
|
||||
setOverlayVisible(true);
|
||||
assert.deepEqual(calls, ['set:true']);
|
||||
});
|
||||
|
||||
test('toggle overlay handler delegates to visible toggle', () => {
|
||||
const calls: string[] = [];
|
||||
const toggleOverlay = createToggleOverlayHandler({
|
||||
toggleVisibleOverlay: () => calls.push('toggle'),
|
||||
});
|
||||
|
||||
toggleOverlay();
|
||||
assert.deepEqual(calls, ['toggle']);
|
||||
});
|
||||
|
||||
test('overlay modal closed handler delegates to runtime handler', () => {
|
||||
const calls: string[] = [];
|
||||
const handleClosed = createHandleOverlayModalClosedHandler({
|
||||
handleOverlayModalClosedRuntime: (modal) => calls.push(`closed:${modal}`),
|
||||
});
|
||||
|
||||
handleClosed('runtime-options');
|
||||
assert.deepEqual(calls, ['closed:runtime-options']);
|
||||
});
|
||||
|
||||
test('append clipboard queue handler forwards runtime deps and result', () => {
|
||||
const calls: string[] = [];
|
||||
const mpvClient = { connected: true };
|
||||
const appendClipboardVideoToQueue = createAppendClipboardVideoToQueueHandler({
|
||||
appendClipboardVideoToQueueRuntime: (options) => {
|
||||
assert.equal(options.getMpvClient(), mpvClient);
|
||||
assert.equal(options.readClipboardText(), '/tmp/video.mkv');
|
||||
options.showMpvOsd('queued');
|
||||
options.sendMpvCommand(['loadfile', '/tmp/video.mkv', 'append']);
|
||||
return { ok: true, message: 'ok' };
|
||||
},
|
||||
getMpvClient: () => mpvClient,
|
||||
readClipboardText: () => '/tmp/video.mkv',
|
||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||
sendMpvCommand: (command) => calls.push(`mpv:${command.join(':')}`),
|
||||
});
|
||||
|
||||
const result = appendClipboardVideoToQueue();
|
||||
assert.deepEqual(result, { ok: true, message: 'ok' });
|
||||
assert.deepEqual(calls, ['osd:queued', 'mpv:loadfile:/tmp/video.mkv:append']);
|
||||
});
|
||||
47
src/main/runtime/overlay-main-actions.ts
Normal file
47
src/main/runtime/overlay-main-actions.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { OverlayHostedModal } from '../overlay-runtime';
|
||||
import type { AppendClipboardVideoToQueueRuntimeDeps } from './clipboard-queue';
|
||||
|
||||
export function createSetOverlayVisibleHandler(deps: {
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
}) {
|
||||
return (visible: boolean): void => {
|
||||
deps.setVisibleOverlayVisible(visible);
|
||||
};
|
||||
}
|
||||
|
||||
export function createToggleOverlayHandler(deps: {
|
||||
toggleVisibleOverlay: () => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.toggleVisibleOverlay();
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleOverlayModalClosedHandler(deps: {
|
||||
handleOverlayModalClosedRuntime: (modal: OverlayHostedModal) => void;
|
||||
}) {
|
||||
return (modal: OverlayHostedModal): void => {
|
||||
deps.handleOverlayModalClosedRuntime(modal);
|
||||
};
|
||||
}
|
||||
|
||||
export function createAppendClipboardVideoToQueueHandler(deps: {
|
||||
appendClipboardVideoToQueueRuntime: (
|
||||
options: AppendClipboardVideoToQueueRuntimeDeps,
|
||||
) => { ok: boolean; message: string };
|
||||
getMpvClient: () => AppendClipboardVideoToQueueRuntimeDeps['getMpvClient'] extends () => infer T
|
||||
? T
|
||||
: never;
|
||||
readClipboardText: () => string;
|
||||
showMpvOsd: (text: string) => void;
|
||||
sendMpvCommand: (command: (string | number)[]) => void;
|
||||
}) {
|
||||
return (): { ok: boolean; message: string } => {
|
||||
return deps.appendClipboardVideoToQueueRuntime({
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
readClipboardText: deps.readClipboardText,
|
||||
showMpvOsd: deps.showMpvOsd,
|
||||
sendMpvCommand: deps.sendMpvCommand,
|
||||
});
|
||||
};
|
||||
}
|
||||
70
src/main/runtime/overlay-runtime-options.test.ts
Normal file
70
src/main/runtime/overlay-runtime-options.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
|
||||
|
||||
test('build initialize overlay runtime options maps dependencies', () => {
|
||||
const calls: string[] = [];
|
||||
const buildOptions = createBuildInitializeOverlayRuntimeOptionsHandler({
|
||||
getBackendOverride: () => 'x11',
|
||||
getInitialInvisibleOverlayVisibility: () => true,
|
||||
createMainWindow: () => calls.push('create-main'),
|
||||
createInvisibleWindow: () => calls.push('create-invisible'),
|
||||
registerGlobalShortcuts: () => calls.push('register-shortcuts'),
|
||||
updateVisibleOverlayBounds: () => calls.push('update-visible-bounds'),
|
||||
updateInvisibleOverlayBounds: () => calls.push('update-invisible-bounds'),
|
||||
isVisibleOverlayVisible: () => true,
|
||||
isInvisibleOverlayVisible: () => false,
|
||||
updateVisibleOverlayVisibility: () => calls.push('update-visible'),
|
||||
updateInvisibleOverlayVisibility: () => calls.push('update-invisible'),
|
||||
getOverlayWindows: () => [],
|
||||
syncOverlayShortcuts: () => calls.push('sync-shortcuts'),
|
||||
setWindowTracker: () => calls.push('set-tracker'),
|
||||
getResolvedConfig: () => ({}),
|
||||
getSubtitleTimingTracker: () => null,
|
||||
getMpvClient: () => null,
|
||||
getMpvSocketPath: () => '/tmp/mpv.sock',
|
||||
getRuntimeOptionsManager: () => null,
|
||||
setAnkiIntegration: () => calls.push('set-anki'),
|
||||
showDesktopNotification: () => calls.push('notify'),
|
||||
createFieldGroupingCallback: () => async () => ({
|
||||
keepNoteId: 1,
|
||||
deleteNoteId: 2,
|
||||
deleteDuplicate: false,
|
||||
cancelled: false,
|
||||
}),
|
||||
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
|
||||
});
|
||||
|
||||
const options = buildOptions();
|
||||
assert.equal(options.backendOverride, 'x11');
|
||||
assert.equal(options.getInitialInvisibleOverlayVisibility(), true);
|
||||
assert.equal(options.isVisibleOverlayVisible(), true);
|
||||
assert.equal(options.isInvisibleOverlayVisible(), false);
|
||||
assert.equal(options.getMpvSocketPath(), '/tmp/mpv.sock');
|
||||
assert.equal(options.getKnownWordCacheStatePath(), '/tmp/known-words-cache.json');
|
||||
options.createMainWindow();
|
||||
options.createInvisibleWindow();
|
||||
options.registerGlobalShortcuts();
|
||||
options.updateVisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||
options.updateInvisibleOverlayBounds({ x: 0, y: 0, width: 10, height: 10 });
|
||||
options.updateVisibleOverlayVisibility();
|
||||
options.updateInvisibleOverlayVisibility();
|
||||
options.syncOverlayShortcuts();
|
||||
options.setWindowTracker(null);
|
||||
options.setAnkiIntegration(null);
|
||||
options.showDesktopNotification('title', {});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'create-main',
|
||||
'create-invisible',
|
||||
'register-shortcuts',
|
||||
'update-visible-bounds',
|
||||
'update-invisible-bounds',
|
||||
'update-visible',
|
||||
'update-invisible',
|
||||
'sync-shortcuts',
|
||||
'set-tracker',
|
||||
'set-anki',
|
||||
'notify',
|
||||
]);
|
||||
});
|
||||
93
src/main/runtime/overlay-runtime-options.ts
Normal file
93
src/main/runtime/overlay-runtime-options.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
AnkiConnectConfig,
|
||||
KikuFieldGroupingChoice,
|
||||
KikuFieldGroupingRequestData,
|
||||
WindowGeometry,
|
||||
} from '../../types';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
type OverlayRuntimeOptions = {
|
||||
backendOverride: string | null;
|
||||
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
isInvisibleOverlayVisible: () => boolean;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
getMpvSocketPath: () => string;
|
||||
getRuntimeOptionsManager: () => {
|
||||
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||
} | null;
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
};
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
|
||||
getBackendOverride: () => string | null;
|
||||
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
registerGlobalShortcuts: () => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
isInvisibleOverlayVisible: () => boolean;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
getOverlayWindows: () => BrowserWindow[];
|
||||
syncOverlayShortcuts: () => void;
|
||||
setWindowTracker: (tracker: unknown | null) => void;
|
||||
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
|
||||
getSubtitleTimingTracker: () => unknown | null;
|
||||
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
|
||||
getMpvSocketPath: () => string;
|
||||
getRuntimeOptionsManager: () => {
|
||||
getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig;
|
||||
} | null;
|
||||
setAnkiIntegration: (integration: unknown | null) => void;
|
||||
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
|
||||
createFieldGroupingCallback: () => (
|
||||
data: KikuFieldGroupingRequestData,
|
||||
) => Promise<KikuFieldGroupingChoice>;
|
||||
getKnownWordCacheStatePath: () => string;
|
||||
}) {
|
||||
return (): OverlayRuntimeOptions => ({
|
||||
backendOverride: deps.getBackendOverride(),
|
||||
getInitialInvisibleOverlayVisibility: deps.getInitialInvisibleOverlayVisibility,
|
||||
createMainWindow: deps.createMainWindow,
|
||||
createInvisibleWindow: deps.createInvisibleWindow,
|
||||
registerGlobalShortcuts: deps.registerGlobalShortcuts,
|
||||
updateVisibleOverlayBounds: deps.updateVisibleOverlayBounds,
|
||||
updateInvisibleOverlayBounds: deps.updateInvisibleOverlayBounds,
|
||||
isVisibleOverlayVisible: deps.isVisibleOverlayVisible,
|
||||
isInvisibleOverlayVisible: deps.isInvisibleOverlayVisible,
|
||||
updateVisibleOverlayVisibility: deps.updateVisibleOverlayVisibility,
|
||||
updateInvisibleOverlayVisibility: deps.updateInvisibleOverlayVisibility,
|
||||
getOverlayWindows: deps.getOverlayWindows,
|
||||
syncOverlayShortcuts: deps.syncOverlayShortcuts,
|
||||
setWindowTracker: deps.setWindowTracker,
|
||||
getResolvedConfig: deps.getResolvedConfig,
|
||||
getSubtitleTimingTracker: deps.getSubtitleTimingTracker,
|
||||
getMpvClient: deps.getMpvClient,
|
||||
getMpvSocketPath: deps.getMpvSocketPath,
|
||||
getRuntimeOptionsManager: deps.getRuntimeOptionsManager,
|
||||
setAnkiIntegration: deps.setAnkiIntegration,
|
||||
showDesktopNotification: deps.showDesktopNotification,
|
||||
createFieldGroupingCallback: deps.createFieldGroupingCallback,
|
||||
getKnownWordCacheStatePath: deps.getKnownWordCacheStatePath,
|
||||
});
|
||||
}
|
||||
66
src/main/runtime/overlay-window-factory.test.ts
Normal file
66
src/main/runtime/overlay-window-factory.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createCreateInvisibleWindowHandler,
|
||||
createCreateMainWindowHandler,
|
||||
createCreateOverlayWindowHandler,
|
||||
} from './overlay-window-factory';
|
||||
|
||||
test('create overlay window handler forwards options and kind', () => {
|
||||
const calls: string[] = [];
|
||||
const window = { id: 1 };
|
||||
const createOverlayWindow = createCreateOverlayWindowHandler({
|
||||
createOverlayWindowCore: (kind, options) => {
|
||||
calls.push(`kind:${kind}`);
|
||||
assert.equal(options.isDev, true);
|
||||
assert.equal(options.overlayDebugVisualizationEnabled, false);
|
||||
assert.equal(options.isOverlayVisible('visible'), true);
|
||||
assert.equal(options.isOverlayVisible('invisible'), false);
|
||||
options.onRuntimeOptionsChanged();
|
||||
options.setOverlayDebugVisualizationEnabled(true);
|
||||
options.onWindowClosed(kind);
|
||||
return window;
|
||||
},
|
||||
isDev: true,
|
||||
getOverlayDebugVisualizationEnabled: () => false,
|
||||
ensureOverlayWindowLevel: () => {},
|
||||
onRuntimeOptionsChanged: () => calls.push('runtime-options'),
|
||||
setOverlayDebugVisualizationEnabled: (enabled) => calls.push(`debug:${enabled}`),
|
||||
isOverlayVisible: (kind) => kind === 'visible',
|
||||
tryHandleOverlayShortcutLocalFallback: () => false,
|
||||
onWindowClosed: (kind) => calls.push(`closed:${kind}`),
|
||||
});
|
||||
|
||||
assert.equal(createOverlayWindow('visible'), window);
|
||||
assert.deepEqual(calls, ['kind:visible', 'runtime-options', 'debug:true', 'closed:visible']);
|
||||
});
|
||||
|
||||
test('create main window handler stores visible window', () => {
|
||||
const calls: string[] = [];
|
||||
const visibleWindow = { id: 'visible' };
|
||||
const createMainWindow = createCreateMainWindowHandler({
|
||||
createOverlayWindow: (kind) => {
|
||||
calls.push(`create:${kind}`);
|
||||
return visibleWindow;
|
||||
},
|
||||
setMainWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||
});
|
||||
|
||||
assert.equal(createMainWindow(), visibleWindow);
|
||||
assert.deepEqual(calls, ['create:visible', 'set:visible']);
|
||||
});
|
||||
|
||||
test('create invisible window handler stores invisible window', () => {
|
||||
const calls: string[] = [];
|
||||
const invisibleWindow = { id: 'invisible' };
|
||||
const createInvisibleWindow = createCreateInvisibleWindowHandler({
|
||||
createOverlayWindow: (kind) => {
|
||||
calls.push(`create:${kind}`);
|
||||
return invisibleWindow;
|
||||
},
|
||||
setInvisibleWindow: (window) => calls.push(`set:${(window as { id: string }).id}`),
|
||||
});
|
||||
|
||||
assert.equal(createInvisibleWindow(), invisibleWindow);
|
||||
assert.deepEqual(calls, ['create:invisible', 'set:invisible']);
|
||||
});
|
||||
60
src/main/runtime/overlay-window-factory.ts
Normal file
60
src/main/runtime/overlay-window-factory.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
type OverlayWindowKind = 'visible' | 'invisible';
|
||||
|
||||
export function createCreateOverlayWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindowCore: (
|
||||
kind: OverlayWindowKind,
|
||||
options: {
|
||||
isDev: boolean;
|
||||
overlayDebugVisualizationEnabled: boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: OverlayWindowKind) => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: OverlayWindowKind) => void;
|
||||
},
|
||||
) => TWindow;
|
||||
isDev: boolean;
|
||||
getOverlayDebugVisualizationEnabled: () => boolean;
|
||||
ensureOverlayWindowLevel: (window: TWindow) => void;
|
||||
onRuntimeOptionsChanged: () => void;
|
||||
setOverlayDebugVisualizationEnabled: (enabled: boolean) => void;
|
||||
isOverlayVisible: (windowKind: OverlayWindowKind) => boolean;
|
||||
tryHandleOverlayShortcutLocalFallback: (input: Electron.Input) => boolean;
|
||||
onWindowClosed: (windowKind: OverlayWindowKind) => void;
|
||||
}) {
|
||||
return (kind: OverlayWindowKind): TWindow => {
|
||||
return deps.createOverlayWindowCore(kind, {
|
||||
isDev: deps.isDev,
|
||||
overlayDebugVisualizationEnabled: deps.getOverlayDebugVisualizationEnabled(),
|
||||
ensureOverlayWindowLevel: deps.ensureOverlayWindowLevel,
|
||||
onRuntimeOptionsChanged: deps.onRuntimeOptionsChanged,
|
||||
setOverlayDebugVisualizationEnabled: deps.setOverlayDebugVisualizationEnabled,
|
||||
isOverlayVisible: deps.isOverlayVisible,
|
||||
tryHandleOverlayShortcutLocalFallback: deps.tryHandleOverlayShortcutLocalFallback,
|
||||
onWindowClosed: deps.onWindowClosed,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createCreateMainWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||
setMainWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return (): TWindow => {
|
||||
const window = deps.createOverlayWindow('visible');
|
||||
deps.setMainWindow(window);
|
||||
return window;
|
||||
};
|
||||
}
|
||||
|
||||
export function createCreateInvisibleWindowHandler<TWindow>(deps: {
|
||||
createOverlayWindow: (kind: OverlayWindowKind) => TWindow;
|
||||
setInvisibleWindow: (window: TWindow | null) => void;
|
||||
}) {
|
||||
return (): TWindow => {
|
||||
const window = deps.createOverlayWindow('invisible');
|
||||
deps.setInvisibleWindow(window);
|
||||
return window;
|
||||
};
|
||||
}
|
||||
76
src/main/runtime/tray-main-actions.test.ts
Normal file
76
src/main/runtime/tray-main-actions.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createBuildTrayMenuTemplateHandler,
|
||||
createResolveTrayIconPathHandler,
|
||||
} from './tray-main-actions';
|
||||
|
||||
test('resolve tray icon path handler forwards runtime dependencies', () => {
|
||||
const calls: string[] = [];
|
||||
const resolveTrayIconPath = createResolveTrayIconPathHandler({
|
||||
resolveTrayIconPathRuntime: (options) => {
|
||||
calls.push(`platform:${options.platform}`);
|
||||
calls.push(`resources:${options.resourcesPath}`);
|
||||
calls.push(`app:${options.appPath}`);
|
||||
calls.push(`dir:${options.dirname}`);
|
||||
calls.push(`join:${options.joinPath('a', 'b')}`);
|
||||
calls.push(`exists:${options.fileExists('/tmp/icon.png')}`);
|
||||
return '/tmp/icon.png';
|
||||
},
|
||||
platform: 'darwin',
|
||||
resourcesPath: '/resources',
|
||||
appPath: '/app',
|
||||
dirname: '/dir',
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
fileExists: () => true,
|
||||
});
|
||||
|
||||
assert.equal(resolveTrayIconPath(), '/tmp/icon.png');
|
||||
assert.deepEqual(calls, [
|
||||
'platform:darwin',
|
||||
'resources:/resources',
|
||||
'app:/app',
|
||||
'dir:/dir',
|
||||
'join:a/b',
|
||||
'exists:true',
|
||||
]);
|
||||
});
|
||||
|
||||
test('build tray template handler wires actions and init guards', () => {
|
||||
const calls: string[] = [];
|
||||
let initialized = false;
|
||||
const buildTemplate = createBuildTrayMenuTemplateHandler({
|
||||
buildTrayMenuTemplateRuntime: (handlers) => {
|
||||
handlers.openOverlay();
|
||||
handlers.openYomitanSettings();
|
||||
handlers.openRuntimeOptions();
|
||||
handlers.openJellyfinSetup();
|
||||
handlers.openAnilistSetup();
|
||||
handlers.quitApp();
|
||||
return [{ label: 'ok' }] as never;
|
||||
},
|
||||
initializeOverlayRuntime: () => {
|
||||
initialized = true;
|
||||
calls.push('init');
|
||||
},
|
||||
isOverlayRuntimeInitialized: () => initialized,
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||
openYomitanSettings: () => calls.push('yomitan'),
|
||||
openRuntimeOptionsPalette: () => calls.push('runtime-options'),
|
||||
openJellyfinSetupWindow: () => calls.push('jellyfin'),
|
||||
openAnilistSetupWindow: () => calls.push('anilist'),
|
||||
quitApp: () => calls.push('quit'),
|
||||
});
|
||||
|
||||
const template = buildTemplate();
|
||||
assert.deepEqual(template, [{ label: 'ok' }]);
|
||||
assert.deepEqual(calls, [
|
||||
'init',
|
||||
'visible:true',
|
||||
'yomitan',
|
||||
'runtime-options',
|
||||
'jellyfin',
|
||||
'anilist',
|
||||
'quit',
|
||||
]);
|
||||
});
|
||||
75
src/main/runtime/tray-main-actions.ts
Normal file
75
src/main/runtime/tray-main-actions.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export function createResolveTrayIconPathHandler(deps: {
|
||||
resolveTrayIconPathRuntime: (options: {
|
||||
platform: string;
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
dirname: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
fileExists: (path: string) => boolean;
|
||||
}) => string | null;
|
||||
platform: string;
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
dirname: string;
|
||||
joinPath: (...parts: string[]) => string;
|
||||
fileExists: (path: string) => boolean;
|
||||
}) {
|
||||
return (): string | null => {
|
||||
return deps.resolveTrayIconPathRuntime({
|
||||
platform: deps.platform,
|
||||
resourcesPath: deps.resourcesPath,
|
||||
appPath: deps.appPath,
|
||||
dirname: deps.dirname,
|
||||
joinPath: deps.joinPath,
|
||||
fileExists: deps.fileExists,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createBuildTrayMenuTemplateHandler<TMenuItem>(deps: {
|
||||
buildTrayMenuTemplateRuntime: (handlers: {
|
||||
openOverlay: () => void;
|
||||
openYomitanSettings: () => void;
|
||||
openRuntimeOptions: () => void;
|
||||
openJellyfinSetup: () => void;
|
||||
openAnilistSetup: () => void;
|
||||
quitApp: () => void;
|
||||
}) => TMenuItem[];
|
||||
initializeOverlayRuntime: () => void;
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
openYomitanSettings: () => void;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
openJellyfinSetupWindow: () => void;
|
||||
openAnilistSetupWindow: () => void;
|
||||
quitApp: () => void;
|
||||
}) {
|
||||
return (): TMenuItem[] => {
|
||||
return deps.buildTrayMenuTemplateRuntime({
|
||||
openOverlay: () => {
|
||||
if (!deps.isOverlayRuntimeInitialized()) {
|
||||
deps.initializeOverlayRuntime();
|
||||
}
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
},
|
||||
openYomitanSettings: () => {
|
||||
deps.openYomitanSettings();
|
||||
},
|
||||
openRuntimeOptions: () => {
|
||||
if (!deps.isOverlayRuntimeInitialized()) {
|
||||
deps.initializeOverlayRuntime();
|
||||
}
|
||||
deps.openRuntimeOptionsPalette();
|
||||
},
|
||||
openJellyfinSetup: () => {
|
||||
deps.openJellyfinSetupWindow();
|
||||
},
|
||||
openAnilistSetup: () => {
|
||||
deps.openAnilistSetupWindow();
|
||||
},
|
||||
quitApp: () => {
|
||||
deps.quitApp();
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
86
src/main/runtime/yomitan-extension-loader.test.ts
Normal file
86
src/main/runtime/yomitan-extension-loader.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
createEnsureYomitanExtensionLoadedHandler,
|
||||
createLoadYomitanExtensionHandler,
|
||||
} from './yomitan-extension-loader';
|
||||
|
||||
test('load yomitan extension handler forwards parser state dependencies', async () => {
|
||||
const calls: string[] = [];
|
||||
const parserWindow = {} as never;
|
||||
const extension = { id: 'ext' } as never;
|
||||
const loadYomitanExtension = createLoadYomitanExtensionHandler({
|
||||
loadYomitanExtensionCore: async (options) => {
|
||||
calls.push(`path:${options.userDataPath}`);
|
||||
assert.equal(options.getYomitanParserWindow(), parserWindow);
|
||||
options.setYomitanParserWindow(null);
|
||||
options.setYomitanParserReadyPromise(null);
|
||||
options.setYomitanParserInitPromise(null);
|
||||
options.setYomitanExtension(extension);
|
||||
return extension;
|
||||
},
|
||||
userDataPath: '/tmp/subminer',
|
||||
getYomitanParserWindow: () => parserWindow,
|
||||
setYomitanParserWindow: () => calls.push('set-window'),
|
||||
setYomitanParserReadyPromise: () => calls.push('set-ready'),
|
||||
setYomitanParserInitPromise: () => calls.push('set-init'),
|
||||
setYomitanExtension: () => calls.push('set-ext'),
|
||||
});
|
||||
|
||||
assert.equal(await loadYomitanExtension(), extension);
|
||||
assert.deepEqual(calls, ['path:/tmp/subminer', 'set-window', 'set-ready', 'set-init', 'set-ext']);
|
||||
});
|
||||
|
||||
test('ensure yomitan loader returns existing extension when available', async () => {
|
||||
const extension = { id: 'ext' } as never;
|
||||
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||
getYomitanExtension: () => extension,
|
||||
getLoadInFlight: () => null,
|
||||
setLoadInFlight: () => {
|
||||
throw new Error('unexpected');
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
throw new Error('unexpected');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(await ensureLoaded(), extension);
|
||||
});
|
||||
|
||||
test('ensure yomitan loader reuses in-flight promise', async () => {
|
||||
const extension = { id: 'ext' } as never;
|
||||
const inflight = Promise.resolve(extension);
|
||||
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||
getYomitanExtension: () => null,
|
||||
getLoadInFlight: () => inflight,
|
||||
setLoadInFlight: () => {
|
||||
throw new Error('unexpected');
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
throw new Error('unexpected');
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(await ensureLoaded(), extension);
|
||||
});
|
||||
|
||||
test('ensure yomitan loader starts load and clears in-flight when done', async () => {
|
||||
const calls: string[] = [];
|
||||
let inFlight: Promise<any> | null = null;
|
||||
const extension = { id: 'ext' } as never;
|
||||
const ensureLoaded = createEnsureYomitanExtensionLoadedHandler({
|
||||
getYomitanExtension: () => null,
|
||||
getLoadInFlight: () => inFlight,
|
||||
setLoadInFlight: (promise) => {
|
||||
inFlight = promise;
|
||||
calls.push(promise ? 'set:promise' : 'set:null');
|
||||
},
|
||||
loadYomitanExtension: async () => {
|
||||
calls.push('load');
|
||||
return extension;
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(await ensureLoaded(), extension);
|
||||
assert.deepEqual(calls, ['load', 'set:promise', 'set:null']);
|
||||
});
|
||||
48
src/main/runtime/yomitan-extension-loader.ts
Normal file
48
src/main/runtime/yomitan-extension-loader.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Extension } from 'electron';
|
||||
import type { YomitanExtensionLoaderDeps } from '../../core/services/yomitan-extension-loader';
|
||||
|
||||
export function createLoadYomitanExtensionHandler(deps: {
|
||||
loadYomitanExtensionCore: (options: YomitanExtensionLoaderDeps) => Promise<Extension | null>;
|
||||
userDataPath: YomitanExtensionLoaderDeps['userDataPath'];
|
||||
getYomitanParserWindow: YomitanExtensionLoaderDeps['getYomitanParserWindow'];
|
||||
setYomitanParserWindow: YomitanExtensionLoaderDeps['setYomitanParserWindow'];
|
||||
setYomitanParserReadyPromise: YomitanExtensionLoaderDeps['setYomitanParserReadyPromise'];
|
||||
setYomitanParserInitPromise: YomitanExtensionLoaderDeps['setYomitanParserInitPromise'];
|
||||
setYomitanExtension: YomitanExtensionLoaderDeps['setYomitanExtension'];
|
||||
}) {
|
||||
return async (): Promise<Extension | null> => {
|
||||
return deps.loadYomitanExtensionCore({
|
||||
userDataPath: deps.userDataPath,
|
||||
getYomitanParserWindow: deps.getYomitanParserWindow,
|
||||
setYomitanParserWindow: deps.setYomitanParserWindow,
|
||||
setYomitanParserReadyPromise: deps.setYomitanParserReadyPromise,
|
||||
setYomitanParserInitPromise: deps.setYomitanParserInitPromise,
|
||||
setYomitanExtension: deps.setYomitanExtension,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function createEnsureYomitanExtensionLoadedHandler(deps: {
|
||||
getYomitanExtension: () => Extension | null;
|
||||
getLoadInFlight: () => Promise<Extension | null> | null;
|
||||
setLoadInFlight: (promise: Promise<Extension | null> | null) => void;
|
||||
loadYomitanExtension: () => Promise<Extension | null>;
|
||||
}) {
|
||||
return async (): Promise<Extension | null> => {
|
||||
const existing = deps.getYomitanExtension();
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const inFlight = deps.getLoadInFlight();
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const promise = deps.loadYomitanExtension().finally(() => {
|
||||
deps.setLoadInFlight(null);
|
||||
});
|
||||
deps.setLoadInFlight(promise);
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user