mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-27 06:12:05 -07:00
refactor: compose startup and setup window wiring
This commit is contained in:
@@ -83,7 +83,6 @@ This project uses Backlog.md MCP for all task and project management activities.
|
||||
- **When to read it**: BEFORE creating tasks, or when you're unsure whether to track work
|
||||
|
||||
These guides cover:
|
||||
|
||||
- Decision framework for when to create tasks
|
||||
- Search-first workflow to avoid duplicates
|
||||
- Links to detailed guides for task creation, execution, and finalization
|
||||
|
||||
266
src/main.ts
266
src/main.ts
@@ -170,7 +170,6 @@ import {
|
||||
createBuildEnforceOverlayLayerOrderMainDepsHandler,
|
||||
createBuildEnsureOverlayWindowLevelMainDepsHandler,
|
||||
createBuildUpdateVisibleOverlayBoundsMainDepsHandler,
|
||||
createOverlayWindowRuntimeHandlers,
|
||||
createTrayRuntimeHandlers,
|
||||
createOverlayVisibilityRuntime,
|
||||
createBroadcastRuntimeOptionsChangedHandler,
|
||||
@@ -235,11 +234,6 @@ import {
|
||||
createHandleMineSentenceDigitHandler,
|
||||
createHandleMultiCopyDigitHandler,
|
||||
} from './main/runtime/domains/mining';
|
||||
import {
|
||||
createCliCommandContextFactory,
|
||||
createInitialArgsRuntimeHandler,
|
||||
createCliCommandRuntimeHandler,
|
||||
} from './main/runtime/domains/ipc';
|
||||
import {
|
||||
enforceUnsupportedWaylandMode,
|
||||
forceX11Backend,
|
||||
@@ -384,9 +378,12 @@ import {
|
||||
composeAnilistSetupHandlers,
|
||||
composeAnilistTrackingHandlers,
|
||||
composeAppReadyRuntime,
|
||||
composeCliStartupHandlers,
|
||||
composeHeadlessStartupHandlers,
|
||||
composeIpcRuntimeHandlers,
|
||||
composeJellyfinRuntimeHandlers,
|
||||
composeMpvRuntimeHandlers,
|
||||
composeOverlayWindowHandlers,
|
||||
composeShortcutRuntimes,
|
||||
composeStartupLifecycleHandlers,
|
||||
} from './main/runtime/composers';
|
||||
@@ -421,6 +418,11 @@ import { handleCharacterDictionaryAutoSyncComplete } from './main/runtime/charac
|
||||
import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications';
|
||||
import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate';
|
||||
import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer';
|
||||
import {
|
||||
createCreateAnilistSetupWindowHandler,
|
||||
createCreateFirstRunSetupWindowHandler,
|
||||
createCreateJellyfinSetupWindowHandler,
|
||||
} from './main/runtime/setup-window-factory';
|
||||
import { isYoutubePlaybackActive } from './main/runtime/youtube-playback';
|
||||
import { createYomitanProfilePolicy } from './main/runtime/yomitan-profile-policy';
|
||||
import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log';
|
||||
@@ -2360,17 +2362,8 @@ const {
|
||||
getSetupWindow: () => appState.jellyfinSetupWindow,
|
||||
},
|
||||
openJellyfinSetupWindowMainDeps: {
|
||||
createSetupWindow: () =>
|
||||
new BrowserWindow({
|
||||
width: 520,
|
||||
height: 560,
|
||||
title: 'Jellyfin Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
createSetupWindow: createCreateJellyfinSetupWindowHandler({
|
||||
createBrowserWindow: (options) => new BrowserWindow(options),
|
||||
}),
|
||||
buildSetupFormHtml: (defaultServer, defaultUser) =>
|
||||
buildJellyfinSetupFormHtml(defaultServer, defaultUser),
|
||||
@@ -2405,20 +2398,8 @@ const maybeFocusExistingFirstRunSetupWindow = createMaybeFocusExistingFirstRunSe
|
||||
});
|
||||
const openFirstRunSetupWindowHandler = createOpenFirstRunSetupWindowHandler({
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingFirstRunSetupWindow,
|
||||
createSetupWindow: () =>
|
||||
new BrowserWindow({
|
||||
width: 480,
|
||||
height: 460,
|
||||
title: 'SubMiner Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
createSetupWindow: createCreateFirstRunSetupWindowHandler({
|
||||
createBrowserWindow: (options) => new BrowserWindow(options),
|
||||
}),
|
||||
getSetupSnapshot: async () => {
|
||||
const snapshot = await firstRunSetupService.getSetupStatus();
|
||||
@@ -2564,17 +2545,8 @@ const maybeFocusExistingAnilistSetupWindow = createMaybeFocusExistingAnilistSetu
|
||||
const buildOpenAnilistSetupWindowMainDepsHandler = createBuildOpenAnilistSetupWindowMainDepsHandler(
|
||||
{
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingAnilistSetupWindow,
|
||||
createSetupWindow: () =>
|
||||
new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 760,
|
||||
title: 'Anilist Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
createSetupWindow: createCreateAnilistSetupWindowHandler({
|
||||
createBrowserWindow: (options) => new BrowserWindow(options),
|
||||
}),
|
||||
buildAuthorizeUrl: () =>
|
||||
buildAnilistSetupUrl({
|
||||
@@ -3464,88 +3436,6 @@ const { appReadyRuntimeRunner } = composeAppReadyRuntime({
|
||||
immersionTrackerStartupMainDeps,
|
||||
});
|
||||
|
||||
const { runAndApplyStartupState } = runtimeRegistry.startup.createStartupRuntimeHandlers<
|
||||
CliArgs,
|
||||
StartupState,
|
||||
ReturnType<typeof createStartupBootstrapRuntimeDeps>
|
||||
>({
|
||||
appLifecycleRuntimeRunnerMainDeps: {
|
||||
app: appLifecycleApp,
|
||||
platform: process.platform,
|
||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||
handleCliCommand(nextArgs, source),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||
onReady: appReadyRuntimeRunner,
|
||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||
},
|
||||
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
},
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||
enforceUnsupportedWaylandMode(args);
|
||||
},
|
||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
},
|
||||
) => generateDefaultConfigFile(args, options),
|
||||
setExitCode: (code) => {
|
||||
process.exitCode = code;
|
||||
},
|
||||
quitApp: () => requestAppQuit(),
|
||||
logGenerateConfigError: (message) => logger.error(message),
|
||||
startAppLifecycle,
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
|
||||
runStartupBootstrapRuntime,
|
||||
applyStartupState: (startupState) => applyStartupState(appState, startupState),
|
||||
});
|
||||
|
||||
runAndApplyStartupState();
|
||||
if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||
void refreshAnilistClientSecretStateIfEnabled({ force: true });
|
||||
anilistStateRuntime.refreshRetryQueueState();
|
||||
}
|
||||
void initializeDiscordPresenceService();
|
||||
|
||||
const handleCliCommand = createCliCommandRuntimeHandler({
|
||||
handleTexthookerOnlyModeTransitionMainDeps: {
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||
setTexthookerOnlyMode: (enabled) => {
|
||||
appState.texthookerOnlyMode = enabled;
|
||||
},
|
||||
commandNeedsOverlayStartupPrereqs: (inputArgs) => commandNeedsOverlayStartupPrereqs(inputArgs),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
},
|
||||
createCliCommandContext: () => createCliCommandContextHandler(),
|
||||
handleCliCommandRuntimeServiceWithContext: (args, source, cliContext) =>
|
||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext),
|
||||
});
|
||||
|
||||
function ensureOverlayStartupPrereqs(): void {
|
||||
if (appState.subtitlePosition === null) {
|
||||
loadSubtitlePosition();
|
||||
@@ -3590,29 +3480,6 @@ async function ensureYoutubePlaybackRuntimeReady(): Promise<void> {
|
||||
ensureOverlayWindowsReadyForVisibilityActions();
|
||||
}
|
||||
|
||||
const handleInitialArgsRuntimeHandler = createInitialArgsRuntimeHandler({
|
||||
getInitialArgs: () => appState.initialArgs,
|
||||
isBackgroundMode: () => appState.backgroundMode,
|
||||
shouldEnsureTrayOnStartup: () =>
|
||||
shouldEnsureTrayOnStartupForInitialArgs(process.platform, appState.initialArgs),
|
||||
shouldRunHeadlessInitialCommand: (args) => isHeadlessInitialCommand(args),
|
||||
ensureTray: () => ensureTray(),
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
hasImmersionTracker: () => Boolean(appState.immersionTracker),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
commandNeedsOverlayStartupPrereqs: (args) => commandNeedsOverlayStartupPrereqs(args),
|
||||
commandNeedsOverlayRuntime: (args) => commandNeedsOverlayRuntime(args),
|
||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
logInfo: (message) => logger.info(message),
|
||||
handleCliCommand: (args, source) => handleCliCommand(args, source),
|
||||
});
|
||||
|
||||
function handleInitialArgs(): void {
|
||||
handleInitialArgsRuntimeHandler();
|
||||
}
|
||||
|
||||
const {
|
||||
createMpvClientRuntimeService: createMpvClientRuntimeServiceHandler,
|
||||
updateMpvSubtitleRenderMetrics: updateMpvSubtitleRenderMetricsHandler,
|
||||
@@ -4763,7 +4630,8 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
|
||||
registerIpcRuntimeServices,
|
||||
},
|
||||
});
|
||||
const createCliCommandContextHandler = createCliCommandContextFactory({
|
||||
const { handleCliCommand, handleInitialArgs } = composeCliStartupHandlers({
|
||||
cliCommandContextMainDeps: {
|
||||
appState,
|
||||
setLogLevel: (level) => setLogLevel(level, 'cli'),
|
||||
texthookerService,
|
||||
@@ -4814,9 +4682,109 @@ const createCliCommandContextHandler = createCliCommandContextFactory({
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
logWarn: (message: string) => logger.warn(message),
|
||||
logError: (message: string, err: unknown) => logger.error(message, err),
|
||||
},
|
||||
cliCommandRuntimeHandlerMainDeps: {
|
||||
handleTexthookerOnlyModeTransitionMainDeps: {
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||
setTexthookerOnlyMode: (enabled) => {
|
||||
appState.texthookerOnlyMode = enabled;
|
||||
},
|
||||
commandNeedsOverlayStartupPrereqs: (inputArgs) =>
|
||||
commandNeedsOverlayStartupPrereqs(inputArgs),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
logInfo: (message: string) => logger.info(message),
|
||||
},
|
||||
handleCliCommandRuntimeServiceWithContext: (args, source, cliContext) =>
|
||||
handleCliCommandRuntimeServiceWithContext(args, source, cliContext),
|
||||
},
|
||||
initialArgsRuntimeHandlerMainDeps: {
|
||||
getInitialArgs: () => appState.initialArgs,
|
||||
isBackgroundMode: () => appState.backgroundMode,
|
||||
shouldEnsureTrayOnStartup: () =>
|
||||
shouldEnsureTrayOnStartupForInitialArgs(process.platform, appState.initialArgs),
|
||||
shouldRunHeadlessInitialCommand: (args) => isHeadlessInitialCommand(args),
|
||||
ensureTray: () => ensureTray(),
|
||||
isTexthookerOnlyMode: () => appState.texthookerOnlyMode,
|
||||
hasImmersionTracker: () => Boolean(appState.immersionTracker),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
commandNeedsOverlayStartupPrereqs: (args) => commandNeedsOverlayStartupPrereqs(args),
|
||||
commandNeedsOverlayRuntime: (args) => commandNeedsOverlayRuntime(args),
|
||||
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
|
||||
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
|
||||
initializeOverlayRuntime: () => initializeOverlayRuntime(),
|
||||
logInfo: (message) => logger.info(message),
|
||||
},
|
||||
});
|
||||
const { runAndApplyStartupState } = composeHeadlessStartupHandlers<
|
||||
CliArgs,
|
||||
StartupState,
|
||||
ReturnType<typeof createStartupBootstrapRuntimeDeps>
|
||||
>({
|
||||
startupRuntimeHandlersDeps: {
|
||||
appLifecycleRuntimeRunnerMainDeps: {
|
||||
app: appLifecycleApp,
|
||||
platform: process.platform,
|
||||
shouldStartApp: (nextArgs: CliArgs) => shouldStartApp(nextArgs),
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
handleCliCommand: (nextArgs: CliArgs, source: CliCommandSource) =>
|
||||
handleCliCommand(nextArgs, source),
|
||||
printHelp: () => printHelp(DEFAULT_TEXTHOOKER_PORT),
|
||||
logNoRunningInstance: () => appLogger.logNoRunningInstance(),
|
||||
onReady: appReadyRuntimeRunner,
|
||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||
shouldQuitOnWindowAllClosed: () => !appState.backgroundMode,
|
||||
},
|
||||
createAppLifecycleRuntimeRunner: (params) => createAppLifecycleRuntimeRunner(params),
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||
argv: process.argv,
|
||||
parseArgs: (argv: string[]) => parseArgs(argv),
|
||||
setLogLevel: (level: string, source: LogLevelSource) => {
|
||||
setLogLevel(level, source);
|
||||
},
|
||||
forceX11Backend: (args: CliArgs) => {
|
||||
forceX11Backend(args);
|
||||
},
|
||||
enforceUnsupportedWaylandMode: (args: CliArgs) => {
|
||||
enforceUnsupportedWaylandMode(args);
|
||||
},
|
||||
shouldStartApp: (args: CliArgs) => shouldStartApp(args),
|
||||
getDefaultSocketPath: () => getDefaultSocketPath(),
|
||||
defaultTexthookerPort: DEFAULT_TEXTHOOKER_PORT,
|
||||
configDir: CONFIG_DIR,
|
||||
defaultConfig: DEFAULT_CONFIG,
|
||||
generateConfigTemplate: (config: ResolvedConfig) => generateConfigTemplate(config),
|
||||
generateDefaultConfigFile: (
|
||||
args: CliArgs,
|
||||
options: {
|
||||
configDir: string;
|
||||
defaultConfig: unknown;
|
||||
generateTemplate: (config: unknown) => string;
|
||||
},
|
||||
) => generateDefaultConfigFile(args, options),
|
||||
setExitCode: (code) => {
|
||||
process.exitCode = code;
|
||||
},
|
||||
quitApp: () => requestAppQuit(),
|
||||
logGenerateConfigError: (message) => logger.error(message),
|
||||
startAppLifecycle,
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps) => createStartupBootstrapRuntimeDeps(deps),
|
||||
runStartupBootstrapRuntime,
|
||||
applyStartupState: (startupState) => applyStartupState(appState, startupState),
|
||||
},
|
||||
});
|
||||
|
||||
runAndApplyStartupState();
|
||||
if (isAnilistTrackingEnabled(getResolvedConfig())) {
|
||||
void refreshAnilistClientSecretStateIfEnabled({ force: true });
|
||||
anilistStateRuntime.refreshRetryQueueState();
|
||||
}
|
||||
void initializeDiscordPresenceService();
|
||||
const { createMainWindow: createMainWindowHandler, createModalWindow: createModalWindowHandler } =
|
||||
createOverlayWindowRuntimeHandlers<BrowserWindow>({
|
||||
composeOverlayWindowHandlers<BrowserWindow>({
|
||||
createOverlayWindowDeps: {
|
||||
createOverlayWindowCore: (kind, options) => createOverlayWindowCore(kind, options),
|
||||
isDev,
|
||||
|
||||
83
src/main/runtime/composers/cli-startup-composer.test.ts
Normal file
83
src/main/runtime/composers/cli-startup-composer.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeCliStartupHandlers } from './cli-startup-composer';
|
||||
|
||||
test('composeCliStartupHandlers returns callable CLI startup handlers', () => {
|
||||
const handlers = composeCliStartupHandlers({
|
||||
cliCommandContextMainDeps: {
|
||||
appState: {} as never,
|
||||
setLogLevel: () => {},
|
||||
texthookerService: {} as never,
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
openExternal: async () => {},
|
||||
logBrowserOpenError: () => {},
|
||||
showMpvOsd: () => {},
|
||||
initializeOverlayRuntime: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
openFirstRunSetupWindow: () => {},
|
||||
setVisibleOverlayVisible: () => {},
|
||||
copyCurrentSubtitle: () => {},
|
||||
startPendingMultiCopy: () => {},
|
||||
mineSentenceCard: async () => {},
|
||||
startPendingMineSentenceMultiple: () => {},
|
||||
updateLastCardFromClipboard: async () => {},
|
||||
refreshKnownWordCache: async () => {},
|
||||
triggerFieldGrouping: async () => {},
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
getAnilistStatus: () => ({}) as never,
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetupWindow: () => {},
|
||||
openJellyfinSetupWindow: () => {},
|
||||
getAnilistQueueStatus: () => ({}) as never,
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'done' }),
|
||||
generateCharacterDictionary: async () =>
|
||||
({ zipPath: '/tmp/test.zip', fromCache: false, mediaId: 1, mediaTitle: 'Test', entryCount: 1 }),
|
||||
runJellyfinCommand: async () => {},
|
||||
runStatsCommand: async () => {},
|
||||
runYoutubePlaybackFlow: async () => {},
|
||||
openYomitanSettings: () => {},
|
||||
cycleSecondarySubMode: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
printHelp: () => {},
|
||||
stopApp: () => {},
|
||||
hasMainWindow: () => false,
|
||||
getMultiCopyTimeoutMs: () => 0,
|
||||
schedule: () => 0 as never,
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
logError: () => {},
|
||||
},
|
||||
cliCommandRuntimeHandlerMainDeps: {
|
||||
handleTexthookerOnlyModeTransitionMainDeps: {
|
||||
isTexthookerOnlyMode: () => false,
|
||||
ensureOverlayStartupPrereqs: () => {},
|
||||
setTexthookerOnlyMode: () => {},
|
||||
commandNeedsOverlayStartupPrereqs: () => false,
|
||||
startBackgroundWarmups: () => {},
|
||||
logInfo: () => {},
|
||||
},
|
||||
handleCliCommandRuntimeServiceWithContext: () => {},
|
||||
},
|
||||
initialArgsRuntimeHandlerMainDeps: {
|
||||
getInitialArgs: () => null,
|
||||
isBackgroundMode: () => false,
|
||||
shouldEnsureTrayOnStartup: () => false,
|
||||
shouldRunHeadlessInitialCommand: () => false,
|
||||
ensureTray: () => {},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
hasImmersionTracker: () => false,
|
||||
getMpvClient: () => null,
|
||||
commandNeedsOverlayStartupPrereqs: () => false,
|
||||
commandNeedsOverlayRuntime: () => false,
|
||||
ensureOverlayStartupPrereqs: () => {},
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
initializeOverlayRuntime: () => {},
|
||||
logInfo: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof handlers.createCliCommandContext, 'function');
|
||||
assert.equal(typeof handlers.handleCliCommand, 'function');
|
||||
assert.equal(typeof handlers.handleInitialArgs, 'function');
|
||||
});
|
||||
50
src/main/runtime/composers/cli-startup-composer.ts
Normal file
50
src/main/runtime/composers/cli-startup-composer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { CliArgs, CliCommandSource } from '../../../cli/args';
|
||||
import { createCliCommandContextFactory } from '../cli-command-context-factory';
|
||||
import { createCliCommandRuntimeHandler } from '../cli-command-runtime-handler';
|
||||
import { createInitialArgsRuntimeHandler } from '../initial-args-runtime-handler';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type CliCommandContextMainDeps = Parameters<typeof createCliCommandContextFactory>[0];
|
||||
type CliCommandContext = ReturnType<ReturnType<typeof createCliCommandContextFactory>>;
|
||||
type CliCommandRuntimeHandlerMainDeps = Omit<
|
||||
Parameters<typeof createCliCommandRuntimeHandler<CliCommandContext>>[0],
|
||||
'createCliCommandContext'
|
||||
>;
|
||||
type InitialArgsRuntimeHandlerMainDeps = Omit<
|
||||
Parameters<typeof createInitialArgsRuntimeHandler>[0],
|
||||
'handleCliCommand'
|
||||
>;
|
||||
|
||||
export type CliStartupComposerOptions = ComposerInputs<{
|
||||
cliCommandContextMainDeps: CliCommandContextMainDeps;
|
||||
cliCommandRuntimeHandlerMainDeps: CliCommandRuntimeHandlerMainDeps;
|
||||
initialArgsRuntimeHandlerMainDeps: InitialArgsRuntimeHandlerMainDeps;
|
||||
}>;
|
||||
|
||||
export type CliStartupComposerResult = ComposerOutputs<{
|
||||
createCliCommandContext: () => CliCommandContext;
|
||||
handleCliCommand: (args: CliArgs, source?: CliCommandSource) => void;
|
||||
handleInitialArgs: () => void;
|
||||
}>;
|
||||
|
||||
export function composeCliStartupHandlers(
|
||||
options: CliStartupComposerOptions,
|
||||
): CliStartupComposerResult {
|
||||
const createCliCommandContext = createCliCommandContextFactory(
|
||||
options.cliCommandContextMainDeps,
|
||||
);
|
||||
const handleCliCommand = createCliCommandRuntimeHandler({
|
||||
...options.cliCommandRuntimeHandlerMainDeps,
|
||||
createCliCommandContext: () => createCliCommandContext(),
|
||||
});
|
||||
const handleInitialArgs = createInitialArgsRuntimeHandler({
|
||||
...options.initialArgsRuntimeHandlerMainDeps,
|
||||
handleCliCommand: (args, source) => handleCliCommand(args, source),
|
||||
});
|
||||
|
||||
return {
|
||||
createCliCommandContext,
|
||||
handleCliCommand,
|
||||
handleInitialArgs,
|
||||
};
|
||||
}
|
||||
66
src/main/runtime/composers/headless-startup-composer.test.ts
Normal file
66
src/main/runtime/composers/headless-startup-composer.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { CliArgs } from '../../../cli/args';
|
||||
import { composeHeadlessStartupHandlers } from './headless-startup-composer';
|
||||
|
||||
test('composeHeadlessStartupHandlers returns startup bootstrap handlers', () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const handlers = composeHeadlessStartupHandlers<
|
||||
CliArgs,
|
||||
{ mode: string },
|
||||
{ startAppLifecycle: (args: CliArgs) => void }
|
||||
>({
|
||||
startupRuntimeHandlersDeps: {
|
||||
appLifecycleRuntimeRunnerMainDeps: {
|
||||
app: { on: () => {} } as never,
|
||||
platform: 'darwin',
|
||||
shouldStartApp: () => true,
|
||||
parseArgs: () => ({}) as never,
|
||||
handleCliCommand: () => {},
|
||||
printHelp: () => {},
|
||||
logNoRunningInstance: () => {},
|
||||
onReady: async () => {},
|
||||
onWillQuitCleanup: () => {},
|
||||
shouldRestoreWindowsOnActivate: () => false,
|
||||
restoreWindowsOnActivate: () => {},
|
||||
shouldQuitOnWindowAllClosed: () => false,
|
||||
},
|
||||
createAppLifecycleRuntimeRunner: () => (args) => {
|
||||
calls.push(`lifecycle:${(args as { command?: string }).command ?? 'unknown'}`);
|
||||
},
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle) => ({
|
||||
argv: ['node', 'main.js'],
|
||||
parseArgs: () => ({ command: 'start' }) as never,
|
||||
setLogLevel: () => {},
|
||||
forceX11Backend: () => {},
|
||||
enforceUnsupportedWaylandMode: () => {},
|
||||
shouldStartApp: () => true,
|
||||
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||
defaultTexthookerPort: 5174,
|
||||
configDir: '/tmp/config',
|
||||
defaultConfig: {} as never,
|
||||
generateConfigTemplate: () => 'template',
|
||||
generateDefaultConfigFile: async () => 0,
|
||||
setExitCode: () => {},
|
||||
quitApp: () => {},
|
||||
logGenerateConfigError: () => {},
|
||||
startAppLifecycle: (args) => startAppLifecycle(args as never),
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps) => ({
|
||||
startAppLifecycle: deps.startAppLifecycle,
|
||||
}),
|
||||
runStartupBootstrapRuntime: (deps) => {
|
||||
deps.startAppLifecycle({ command: 'start' } as unknown as CliArgs);
|
||||
return { mode: 'started' };
|
||||
},
|
||||
applyStartupState: (state) => {
|
||||
calls.push(`apply:${state.mode}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof handlers.runAndApplyStartupState, 'function');
|
||||
assert.deepEqual(handlers.runAndApplyStartupState(), { mode: 'started' });
|
||||
assert.deepEqual(calls, ['lifecycle:start', 'apply:started']);
|
||||
});
|
||||
49
src/main/runtime/composers/headless-startup-composer.ts
Normal file
49
src/main/runtime/composers/headless-startup-composer.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createStartupRuntimeHandlers } from '../startup-runtime-handlers';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type StartupRuntimeHandlersDeps<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps> = Parameters<
|
||||
typeof createStartupRuntimeHandlers<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps>
|
||||
>[0];
|
||||
type StartupRuntimeHandlers<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps> = ReturnType<
|
||||
typeof createStartupRuntimeHandlers<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps>
|
||||
>;
|
||||
|
||||
export type HeadlessStartupComposerOptions<
|
||||
TCliArgs,
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps,
|
||||
> = ComposerInputs<{
|
||||
startupRuntimeHandlersDeps: StartupRuntimeHandlersDeps<
|
||||
TCliArgs,
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type HeadlessStartupComposerResult<
|
||||
TCliArgs,
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps,
|
||||
> = ComposerOutputs<
|
||||
Pick<
|
||||
StartupRuntimeHandlers<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps>,
|
||||
'appLifecycleRuntimeRunner' | 'runAndApplyStartupState'
|
||||
>
|
||||
>;
|
||||
|
||||
export function composeHeadlessStartupHandlers<
|
||||
TCliArgs,
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps,
|
||||
>(
|
||||
options: HeadlessStartupComposerOptions<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps>,
|
||||
): HeadlessStartupComposerResult<TCliArgs, TStartupState, TStartupBootstrapRuntimeDeps> {
|
||||
const { appLifecycleRuntimeRunner, runAndApplyStartupState } = createStartupRuntimeHandlers(
|
||||
options.startupRuntimeHandlersDeps,
|
||||
);
|
||||
|
||||
return {
|
||||
appLifecycleRuntimeRunner,
|
||||
runAndApplyStartupState,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
export * from './anilist-setup-composer';
|
||||
export * from './anilist-tracking-composer';
|
||||
export * from './app-ready-composer';
|
||||
export * from './cli-startup-composer';
|
||||
export * from './contracts';
|
||||
export * from './headless-startup-composer';
|
||||
export * from './ipc-runtime-composer';
|
||||
export * from './jellyfin-remote-composer';
|
||||
export * from './jellyfin-runtime-composer';
|
||||
export * from './mpv-runtime-composer';
|
||||
export * from './overlay-window-composer';
|
||||
export * from './shortcuts-runtime-composer';
|
||||
export * from './startup-lifecycle-composer';
|
||||
|
||||
34
src/main/runtime/composers/overlay-window-composer.test.ts
Normal file
34
src/main/runtime/composers/overlay-window-composer.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeOverlayWindowHandlers } from './overlay-window-composer';
|
||||
|
||||
test('composeOverlayWindowHandlers returns overlay window handlers', () => {
|
||||
let mainWindow: { kind: string } | null = null;
|
||||
let modalWindow: { kind: string } | null = null;
|
||||
|
||||
const handlers = composeOverlayWindowHandlers<{ kind: string }>({
|
||||
createOverlayWindowDeps: {
|
||||
createOverlayWindowCore: (kind) => ({ kind }),
|
||||
isDev: false,
|
||||
ensureOverlayWindowLevel: () => {},
|
||||
onRuntimeOptionsChanged: () => {},
|
||||
setOverlayDebugVisualizationEnabled: () => {},
|
||||
isOverlayVisible: (kind) => kind === 'visible',
|
||||
tryHandleOverlayShortcutLocalFallback: () => false,
|
||||
forwardTabToMpv: () => {},
|
||||
onWindowClosed: () => {},
|
||||
getYomitanSession: () => null,
|
||||
},
|
||||
setMainWindow: (window) => {
|
||||
mainWindow = window;
|
||||
},
|
||||
setModalWindow: (window) => {
|
||||
modalWindow = window;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(handlers.createMainWindow(), { kind: 'visible' });
|
||||
assert.deepEqual(mainWindow, { kind: 'visible' });
|
||||
assert.deepEqual(handlers.createModalWindow(), { kind: 'modal' });
|
||||
assert.deepEqual(modalWindow, { kind: 'modal' });
|
||||
});
|
||||
18
src/main/runtime/composers/overlay-window-composer.ts
Normal file
18
src/main/runtime/composers/overlay-window-composer.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createOverlayWindowRuntimeHandlers } from '../overlay-window-runtime-handlers';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type OverlayWindowRuntimeDeps<TWindow> =
|
||||
Parameters<typeof createOverlayWindowRuntimeHandlers<TWindow>>[0];
|
||||
type OverlayWindowRuntimeHandlers<TWindow> = ReturnType<
|
||||
typeof createOverlayWindowRuntimeHandlers<TWindow>
|
||||
>;
|
||||
|
||||
export type OverlayWindowComposerOptions<TWindow> = ComposerInputs<OverlayWindowRuntimeDeps<TWindow>>;
|
||||
export type OverlayWindowComposerResult<TWindow> =
|
||||
ComposerOutputs<OverlayWindowRuntimeHandlers<TWindow>>;
|
||||
|
||||
export function composeOverlayWindowHandlers<TWindow>(
|
||||
options: OverlayWindowComposerOptions<TWindow>,
|
||||
): OverlayWindowComposerResult<TWindow> {
|
||||
return createOverlayWindowRuntimeHandlers<TWindow>(options);
|
||||
}
|
||||
79
src/main/runtime/setup-window-factory.test.ts
Normal file
79
src/main/runtime/setup-window-factory.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createCreateAnilistSetupWindowHandler,
|
||||
createCreateFirstRunSetupWindowHandler,
|
||||
createCreateJellyfinSetupWindowHandler,
|
||||
} from './setup-window-factory';
|
||||
|
||||
test('createCreateFirstRunSetupWindowHandler builds first-run setup window', () => {
|
||||
let options: Electron.BrowserWindowConstructorOptions | null = null;
|
||||
const createSetupWindow = createCreateFirstRunSetupWindowHandler({
|
||||
createBrowserWindow: (nextOptions) => {
|
||||
options = nextOptions;
|
||||
return { id: 'first-run' } as never;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(createSetupWindow(), { id: 'first-run' });
|
||||
assert.deepEqual(options, {
|
||||
width: 480,
|
||||
height: 460,
|
||||
title: 'SubMiner Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('createCreateJellyfinSetupWindowHandler builds jellyfin setup window', () => {
|
||||
let options: Electron.BrowserWindowConstructorOptions | null = null;
|
||||
const createSetupWindow = createCreateJellyfinSetupWindowHandler({
|
||||
createBrowserWindow: (nextOptions) => {
|
||||
options = nextOptions;
|
||||
return { id: 'jellyfin' } as never;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(createSetupWindow(), { id: 'jellyfin' });
|
||||
assert.deepEqual(options, {
|
||||
width: 520,
|
||||
height: 560,
|
||||
title: 'Jellyfin Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('createCreateAnilistSetupWindowHandler builds anilist setup window', () => {
|
||||
let options: Electron.BrowserWindowConstructorOptions | null = null;
|
||||
const createSetupWindow = createCreateAnilistSetupWindowHandler({
|
||||
createBrowserWindow: (nextOptions) => {
|
||||
options = nextOptions;
|
||||
return { id: 'anilist' } as never;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(createSetupWindow(), { id: 'anilist' });
|
||||
assert.deepEqual(options, {
|
||||
width: 1000,
|
||||
height: 760,
|
||||
title: 'Anilist Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
53
src/main/runtime/setup-window-factory.ts
Normal file
53
src/main/runtime/setup-window-factory.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export function createCreateFirstRunSetupWindowHandler<TWindow>(deps: {
|
||||
createBrowserWindow: (options: Electron.BrowserWindowConstructorOptions) => TWindow;
|
||||
}) {
|
||||
return (): TWindow =>
|
||||
deps.createBrowserWindow({
|
||||
width: 480,
|
||||
height: 460,
|
||||
title: 'SubMiner Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createCreateJellyfinSetupWindowHandler<TWindow>(deps: {
|
||||
createBrowserWindow: (options: Electron.BrowserWindowConstructorOptions) => TWindow;
|
||||
}) {
|
||||
return (): TWindow =>
|
||||
deps.createBrowserWindow({
|
||||
width: 520,
|
||||
height: 560,
|
||||
title: 'Jellyfin Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function createCreateAnilistSetupWindowHandler<TWindow>(deps: {
|
||||
createBrowserWindow: (options: Electron.BrowserWindowConstructorOptions) => TWindow;
|
||||
}) {
|
||||
return (): TWindow =>
|
||||
deps.createBrowserWindow({
|
||||
width: 1000,
|
||||
height: 760,
|
||||
title: 'Anilist Setup',
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user