refactor: unify cli and runtime wiring for startup and youtube flow

This commit is contained in:
2026-03-22 18:38:54 -07:00
parent 3fb33af116
commit 7d8d2ae7a7
48 changed files with 1009 additions and 370 deletions

View File

@@ -1,5 +1,6 @@
import { handleCliCommand, createCliCommandDepsRuntime } from '../core/services';
import type { CliArgs, CliCommandSource } from '../cli/args';
import type { YoutubeFlowMode } from '../types';
import {
createCliCommandRuntimeServiceDeps,
CliCommandRuntimeServiceDepsParams,
@@ -38,6 +39,11 @@ export interface CliCommandRuntimeServiceContext {
openJellyfinSetup: CliCommandRuntimeServiceDepsParams['jellyfin']['openSetup'];
runStatsCommand: CliCommandRuntimeServiceDepsParams['jellyfin']['runStatsCommand'];
runJellyfinCommand: CliCommandRuntimeServiceDepsParams['jellyfin']['runCommand'];
runYoutubePlaybackFlow: (request: {
url: string;
mode: YoutubeFlowMode;
source: CliCommandSource;
}) => Promise<void>;
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
openRuntimeOptionsPalette: () => void;
@@ -105,6 +111,11 @@ function createCliCommandDepsFromContext(
runStatsCommand: context.runStatsCommand,
runCommand: context.runJellyfinCommand,
},
app: {
stop: context.stopApp,
hasMainWindow: context.hasMainWindow,
runYoutubePlaybackFlow: context.runYoutubePlaybackFlow,
},
ui: {
openFirstRunSetup: context.openFirstRunSetup,
openYomitanSettings: context.openYomitanSettings,
@@ -112,10 +123,6 @@ function createCliCommandDepsFromContext(
openRuntimeOptionsPalette: context.openRuntimeOptionsPalette,
printHelp: context.printHelp,
},
app: {
stop: context.stopApp,
hasMainWindow: context.hasMainWindow,
},
getMultiCopyTimeoutMs: context.getMultiCopyTimeoutMs,
schedule: context.schedule,
log: context.log,

View File

@@ -57,6 +57,7 @@ export interface MainIpcRuntimeServiceDepsParams {
getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility'];
onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed'];
onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened'];
onYoutubePickerResolve: IpcDepsRuntimeOptions['onYoutubePickerResolve'];
openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings'];
quitApp: IpcDepsRuntimeOptions['quitApp'];
toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay'];
@@ -166,6 +167,11 @@ export interface CliCommandRuntimeServiceDepsParams {
runStatsCommand: CliCommandDepsRuntimeOptions['jellyfin']['runStatsCommand'];
runCommand: CliCommandDepsRuntimeOptions['jellyfin']['runCommand'];
};
app: {
stop: CliCommandDepsRuntimeOptions['app']['stop'];
hasMainWindow: CliCommandDepsRuntimeOptions['app']['hasMainWindow'];
runYoutubePlaybackFlow: CliCommandDepsRuntimeOptions['app']['runYoutubePlaybackFlow'];
};
ui: {
openFirstRunSetup: CliCommandDepsRuntimeOptions['ui']['openFirstRunSetup'];
openYomitanSettings: CliCommandDepsRuntimeOptions['ui']['openYomitanSettings'];
@@ -173,10 +179,6 @@ export interface CliCommandRuntimeServiceDepsParams {
openRuntimeOptionsPalette: CliCommandDepsRuntimeOptions['ui']['openRuntimeOptionsPalette'];
printHelp: CliCommandDepsRuntimeOptions['ui']['printHelp'];
};
app: {
stop: CliCommandDepsRuntimeOptions['app']['stop'];
hasMainWindow: CliCommandDepsRuntimeOptions['app']['hasMainWindow'];
};
getMultiCopyTimeoutMs: CliCommandDepsRuntimeOptions['getMultiCopyTimeoutMs'];
schedule: CliCommandDepsRuntimeOptions['schedule'];
log: CliCommandDepsRuntimeOptions['log'];
@@ -207,6 +209,7 @@ export function createMainIpcRuntimeServiceDeps(
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
onOverlayModalClosed: params.onOverlayModalClosed,
onOverlayModalOpened: params.onOverlayModalOpened,
onYoutubePickerResolve: params.onYoutubePickerResolve,
openYomitanSettings: params.openYomitanSettings,
quitApp: params.quitApp,
toggleVisibleOverlay: params.toggleVisibleOverlay,
@@ -324,6 +327,11 @@ export function createCliCommandRuntimeServiceDeps(
runStatsCommand: params.jellyfin.runStatsCommand,
runCommand: params.jellyfin.runCommand,
},
app: {
stop: params.app.stop,
hasMainWindow: params.app.hasMainWindow,
runYoutubePlaybackFlow: params.app.runYoutubePlaybackFlow,
},
ui: {
openFirstRunSetup: params.ui.openFirstRunSetup,
openYomitanSettings: params.ui.openYomitanSettings,
@@ -331,10 +339,6 @@ export function createCliCommandRuntimeServiceDeps(
openRuntimeOptionsPalette: params.ui.openRuntimeOptionsPalette,
printHelp: params.ui.printHelp,
},
app: {
stop: params.app.stop,
hasMainWindow: params.app.hasMainWindow,
},
getMultiCopyTimeoutMs: params.getMultiCopyTimeoutMs,
schedule: params.schedule,
log: params.log,

View File

@@ -275,6 +275,82 @@ test('sendToActiveOverlayWindow prefers visible main overlay window for modal op
assert.deepEqual(mainWindow.sent, [['runtime-options:open']]);
});
test('sendToActiveOverlayWindow can prefer modal window even when main overlay is visible', () => {
const mainWindow = createMockWindow();
mainWindow.visible = true;
const modalWindow = createMockWindow();
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => mainWindow as never,
getModalWindow: () => modalWindow as never,
createModalWindow: () => modalWindow as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
const sent = runtime.sendToActiveOverlayWindow('youtube:picker-open', { sessionId: 'yt-1' }, {
restoreOnModalClose: 'youtube-track-picker',
preferModalWindow: true,
});
assert.equal(sent, true);
assert.deepEqual(mainWindow.sent, []);
assert.deepEqual(modalWindow.sent, [['youtube:picker-open', { sessionId: 'yt-1' }]]);
});
test('modal window path makes visible main overlay click-through until modal closes', () => {
const mainWindow = createMockWindow();
mainWindow.visible = true;
const modalWindow = createMockWindow();
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => mainWindow as never,
getModalWindow: () => modalWindow as never,
createModalWindow: () => modalWindow as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
const sent = runtime.sendToActiveOverlayWindow('youtube:picker-open', { sessionId: 'yt-1' }, {
restoreOnModalClose: 'youtube-track-picker',
preferModalWindow: true,
});
runtime.notifyOverlayModalOpened('youtube-track-picker');
assert.equal(sent, true);
assert.equal(mainWindow.ignoreMouseEvents, true);
assert.equal(modalWindow.ignoreMouseEvents, false);
runtime.handleOverlayModalClosed('youtube-track-picker');
assert.equal(mainWindow.ignoreMouseEvents, false);
});
test('modal window path hides visible main overlay until modal closes', () => {
const mainWindow = createMockWindow();
mainWindow.visible = true;
const modalWindow = createMockWindow();
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => mainWindow as never,
getModalWindow: () => modalWindow as never,
createModalWindow: () => modalWindow as never,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
runtime.sendToActiveOverlayWindow('youtube:picker-open', { sessionId: 'yt-1' }, {
restoreOnModalClose: 'youtube-track-picker',
preferModalWindow: true,
});
runtime.notifyOverlayModalOpened('youtube-track-picker');
assert.equal(mainWindow.getHideCount(), 1);
assert.equal(mainWindow.isVisible(), false);
runtime.handleOverlayModalClosed('youtube-track-picker');
assert.equal(mainWindow.getShowCount(), 1);
assert.equal(mainWindow.isVisible(), true);
});
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
const window = createMockWindow();
const state: boolean[] = [];
@@ -430,3 +506,33 @@ test('modal fallback reveal keeps mouse events ignored until modal confirms open
runtime.notifyOverlayModalOpened('jimaku');
assert.equal(window.ignoreMouseEvents, false);
});
test('waitForModalOpen resolves true after modal acknowledgement', async () => {
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getModalWindow: () => null,
createModalWindow: () => null,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
runtime.sendToActiveOverlayWindow('youtube:picker-open', { sessionId: 'yt-1' }, {
restoreOnModalClose: 'youtube-track-picker',
});
const pending = runtime.waitForModalOpen('youtube-track-picker', 1000);
runtime.notifyOverlayModalOpened('youtube-track-picker');
assert.equal(await pending, true);
});
test('waitForModalOpen resolves false on timeout', async () => {
const runtime = createOverlayModalRuntimeService({
getMainWindow: () => null,
getModalWindow: () => null,
createModalWindow: () => null,
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
setModalWindowBounds: () => {},
});
assert.equal(await runtime.waitForModalOpen('youtube-track-picker', 5), false);
});

View File

@@ -16,11 +16,15 @@ export interface OverlayModalRuntime {
sendToActiveOverlayWindow: (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
runtimeOptions?: {
restoreOnModalClose?: OverlayHostedModal;
preferModalWindow?: boolean;
},
) => boolean;
openRuntimeOptionsPalette: () => void;
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
notifyOverlayModalOpened: (modal: OverlayHostedModal) => void;
waitForModalOpen: (modal: OverlayHostedModal, timeoutMs: number) => Promise<boolean>;
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
}
@@ -33,7 +37,10 @@ export function createOverlayModalRuntimeService(
options: OverlayModalRuntimeOptions = {},
): OverlayModalRuntime {
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
const modalOpenWaiters = new Map<OverlayHostedModal, Array<(opened: boolean) => void>>();
let modalActive = false;
let mainWindowMousePassthroughForcedByModal = false;
let mainWindowHiddenByModal = false;
let pendingModalWindowReveal: BrowserWindow | null = null;
let pendingModalWindowRevealTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -163,6 +170,54 @@ export function createOverlayModalRuntimeService(
pendingModalWindowReveal = null;
};
const setMainWindowMousePassthroughForModal = (enabled: boolean): void => {
const mainWindow = deps.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
mainWindowMousePassthroughForcedByModal = false;
return;
}
if (enabled) {
if (!mainWindow.isVisible()) {
mainWindowMousePassthroughForcedByModal = false;
return;
}
mainWindow.setIgnoreMouseEvents(true, { forward: true });
mainWindowMousePassthroughForcedByModal = true;
return;
}
if (!mainWindowMousePassthroughForcedByModal) {
return;
}
mainWindow.setIgnoreMouseEvents(false);
mainWindowMousePassthroughForcedByModal = false;
};
const setMainWindowVisibilityForModal = (hidden: boolean): void => {
const mainWindow = deps.getMainWindow();
if (!mainWindow || mainWindow.isDestroyed()) {
mainWindowHiddenByModal = false;
return;
}
if (hidden) {
if (!mainWindow.isVisible()) {
mainWindowHiddenByModal = false;
return;
}
mainWindow.hide();
mainWindowHiddenByModal = true;
return;
}
if (!mainWindowHiddenByModal) {
return;
}
mainWindow.show();
mainWindowHiddenByModal = false;
};
const scheduleModalWindowReveal = (window: BrowserWindow): void => {
pendingModalWindowReveal = window;
if (pendingModalWindowRevealTimeout !== null) {
@@ -182,9 +237,13 @@ export function createOverlayModalRuntimeService(
const sendToActiveOverlayWindow = (
channel: string,
payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: OverlayHostedModal },
runtimeOptions?: {
restoreOnModalClose?: OverlayHostedModal;
preferModalWindow?: boolean;
},
): boolean => {
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
const preferModalWindow = runtimeOptions?.preferModalWindow === true;
const sendNow = (window: BrowserWindow): void => {
ensureModalWindowInteractive(window);
@@ -198,7 +257,7 @@ export function createOverlayModalRuntimeService(
if (restoreOnModalClose) {
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
const mainWindow = getTargetOverlayWindow();
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()) {
if (!preferModalWindow && mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()) {
sendOrQueueForWindow(mainWindow, (window) => {
if (payload === undefined) {
window.webContents.send(channel);
@@ -255,6 +314,8 @@ export function createOverlayModalRuntimeService(
if (restoreVisibleOverlayOnModalClose.size === 0) {
clearPendingModalWindowReveal();
notifyModalStateChange(false);
setMainWindowMousePassthroughForModal(false);
setMainWindowVisibilityForModal(false);
if (modalWindow && !modalWindow.isDestroyed()) {
modalWindow.hide();
}
@@ -263,6 +324,11 @@ export function createOverlayModalRuntimeService(
const notifyOverlayModalOpened = (modal: OverlayHostedModal): void => {
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
const waiters = modalOpenWaiters.get(modal) ?? [];
modalOpenWaiters.delete(modal);
for (const resolve of waiters) {
resolve(true);
}
notifyModalStateChange(true);
const targetWindow = getActiveOverlayWindowForModalInput();
clearPendingModalWindowReveal();
@@ -270,6 +336,12 @@ export function createOverlayModalRuntimeService(
return;
}
const modalWindow = deps.getModalWindow();
if (modalWindow && !modalWindow.isDestroyed() && targetWindow === modalWindow) {
setMainWindowMousePassthroughForModal(true);
setMainWindowVisibilityForModal(true);
}
if (targetWindow.isVisible()) {
targetWindow.setIgnoreMouseEvents(false);
elevateModalWindow(targetWindow);
@@ -285,11 +357,34 @@ export function createOverlayModalRuntimeService(
showModalWindow(targetWindow);
};
const waitForModalOpen = async (
modal: OverlayHostedModal,
timeoutMs: number,
): Promise<boolean> =>
await new Promise<boolean>((resolve) => {
const waiters = modalOpenWaiters.get(modal) ?? [];
const finish = (opened: boolean): void => {
clearTimeout(timeout);
resolve(opened);
};
waiters.push(finish);
modalOpenWaiters.set(modal, waiters);
const timeout = setTimeout(() => {
const current = modalOpenWaiters.get(modal) ?? [];
modalOpenWaiters.set(
modal,
current.filter((candidate) => candidate !== finish),
);
resolve(false);
}, timeoutMs);
});
return {
sendToActiveOverlayWindow,
openRuntimeOptionsPalette,
handleOverlayModalClosed,
notifyOverlayModalOpened,
waitForModalOpen,
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
};
}

View File

@@ -60,6 +60,9 @@ test('build cli command context deps maps handlers and values', () => {
runJellyfinCommand: async () => {
calls.push('run-jellyfin');
},
runYoutubePlaybackFlow: async () => {
calls.push('run-youtube');
},
openYomitanSettings: () => calls.push('yomitan'),
cycleSecondarySubMode: () => calls.push('cycle-secondary'),
openRuntimeOptionsPalette: () => calls.push('runtime-options'),

View File

@@ -36,6 +36,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: CliCommandContextFactoryDeps['runYoutubePlaybackFlow'];
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
openRuntimeOptionsPalette: () => void;
@@ -83,6 +84,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
generateCharacterDictionary: deps.generateCharacterDictionary,
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,
openYomitanSettings: deps.openYomitanSettings,
cycleSecondarySubMode: deps.cycleSecondarySubMode,
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,

View File

@@ -9,6 +9,7 @@ test('cli command context factory composes main deps and context handlers', () =
mpvClient: null,
texthookerPort: 5174,
overlayRuntimeInitialized: false,
youtubePlaybackFlowPending: false,
};
const createContext = createCliCommandContextFactory({
@@ -63,6 +64,7 @@ test('cli command context factory composes main deps and context handlers', () =
}),
runStatsCommand: async () => {},
runJellyfinCommand: async () => {},
runYoutubePlaybackFlow: async () => {},
openYomitanSettings: () => {},
cycleSecondarySubMode: () => {},
openRuntimeOptionsPalette: () => {},

View File

@@ -9,6 +9,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
mpvClient: null,
texthookerPort: 5174,
overlayRuntimeInitialized: false,
youtubePlaybackFlowPending: false,
};
const build = createBuildCliCommandContextMainDepsHandler({
@@ -84,6 +85,9 @@ test('cli command context main deps builder maps state and callbacks', async ()
runJellyfinCommand: async () => {
calls.push('run-jellyfin');
},
runYoutubePlaybackFlow: async () => {
calls.push('run-youtube');
},
openYomitanSettings: () => calls.push('open-yomitan'),
cycleSecondarySubMode: () => calls.push('cycle-secondary'),

View File

@@ -1,4 +1,5 @@
import type { CliArgs } from '../../cli/args';
import type { YoutubeFlowMode } from '../../types';
import type { CliCommandContextFactoryDeps } from './cli-command-context';
type CliCommandContextMainState = {
@@ -41,6 +42,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
generateCharacterDictionary: CliCommandContextFactoryDeps['generateCharacterDictionary'];
runStatsCommand: CliCommandContextFactoryDeps['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: (request: {
url: string;
mode: YoutubeFlowMode;
source: 'initial' | 'second-instance';
}) => Promise<void>;
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
@@ -95,6 +101,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
deps.generateCharacterDictionary(targetPath),
runStatsCommand: (args: CliArgs, source) => deps.runStatsCommand(args, source),
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
runYoutubePlaybackFlow: (request) => deps.runYoutubePlaybackFlow(request),
openYomitanSettings: () => deps.openYomitanSettings(),
cycleSecondarySubMode: () => deps.cycleSecondarySubMode(),
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),

View File

@@ -50,6 +50,7 @@ function createDeps() {
}),
runStatsCommand: async () => {},
runJellyfinCommand: async () => {},
runYoutubePlaybackFlow: async () => {},
openYomitanSettings: () => {},
cycleSecondarySubMode: () => {},
openRuntimeOptionsPalette: () => {},

View File

@@ -1,4 +1,5 @@
import type { CliArgs } from '../../cli/args';
import type { YoutubeFlowMode } from '../../types';
import type {
CliCommandRuntimeServiceContext,
CliCommandRuntimeServiceContextHandlers,
@@ -41,6 +42,11 @@ export type CliCommandContextFactoryDeps = {
generateCharacterDictionary: CliCommandRuntimeServiceContext['generateCharacterDictionary'];
runStatsCommand: CliCommandRuntimeServiceContext['runStatsCommand'];
runJellyfinCommand: (args: CliArgs) => Promise<void>;
runYoutubePlaybackFlow: (request: {
url: string;
mode: YoutubeFlowMode;
source: 'initial' | 'second-instance';
}) => Promise<void>;
openYomitanSettings: () => void;
cycleSecondarySubMode: () => void;
openRuntimeOptionsPalette: () => void;
@@ -95,6 +101,7 @@ export function createCliCommandContext(
generateCharacterDictionary: deps.generateCharacterDictionary,
runStatsCommand: deps.runStatsCommand,
runJellyfinCommand: deps.runJellyfinCommand,
runYoutubePlaybackFlow: deps.runYoutubePlaybackFlow,
openYomitanSettings: deps.openYomitanSettings,
cycleSecondarySubMode: deps.cycleSecondarySubMode,
openRuntimeOptionsPalette: deps.openRuntimeOptionsPalette,

View File

@@ -67,6 +67,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
getAnilistQueueStatus: () => ({}) as never,
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
onYoutubePickerResolve: async () => ({ ok: true, message: 'ok' }),
},
ankiJimakuDeps: {
patchAnkiConnectEnabled: () => {},

View File

@@ -72,6 +72,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -280,6 +281,7 @@ test('composeMpvRuntimeHandlers skips MeCab warmup when all POS-dependent annota
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -411,6 +413,7 @@ test('composeMpvRuntimeHandlers runs tokenization warmup once across sequential
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -550,6 +553,7 @@ test('composeMpvRuntimeHandlers does not block first tokenization on dictionary
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -683,6 +687,7 @@ test('composeMpvRuntimeHandlers shows annotation loading OSD after tokenization-
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},
@@ -830,6 +835,7 @@ test('composeMpvRuntimeHandlers reuses completed background tokenization warmups
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: null,
youtubePlaybackFlowPending: false,
},
getQuitOnDisconnectArmed: () => false,
scheduleQuitCheck: () => {},

View File

@@ -25,6 +25,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
currentSubAssText: '',
playbackPaused: null,
previousSecondarySubVisibility: false,
youtubePlaybackFlowPending: false,
};
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({

View File

@@ -34,6 +34,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
currentSubtitleData?: SubtitleData | null;
playbackPaused: boolean | null;
previousSecondarySubVisibility: boolean | null;
youtubePlaybackFlowPending: boolean;
};
getQuitOnDisconnectArmed: () => boolean;
scheduleQuitCheck: (callback: () => void) => void;

View File

@@ -33,13 +33,17 @@ export function resolveWindowsMpvPath(deps: WindowsMpvLaunchDeps): string {
return '';
}
export function buildWindowsMpvLaunchArgs(targets: string[]): string[] {
return ['--player-operation-mode=pseudo-gui', '--profile=subminer', ...targets];
export function buildWindowsMpvLaunchArgs(
targets: string[],
extraArgs: string[] = [],
): string[] {
return ['--player-operation-mode=pseudo-gui', '--profile=subminer', ...extraArgs, ...targets];
}
export function launchWindowsMpv(
targets: string[],
deps: WindowsMpvLaunchDeps,
extraArgs: string[] = [],
): { ok: boolean; mpvPath: string } {
const mpvPath = resolveWindowsMpvPath(deps);
if (!mpvPath) {
@@ -51,7 +55,7 @@ export function launchWindowsMpv(
}
try {
deps.spawnDetached(mpvPath, buildWindowsMpvLaunchArgs(targets));
deps.spawnDetached(mpvPath, buildWindowsMpvLaunchArgs(targets, extraArgs));
return { ok: true, mpvPath };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);

View File

@@ -188,6 +188,7 @@ export interface AppState {
overlayDebugVisualizationEnabled: boolean;
statsOverlayVisible: boolean;
subsyncInProgress: boolean;
youtubePlaybackFlowPending: boolean;
initialArgs: CliArgs | null;
mpvSocketPath: string;
texthookerPort: number;
@@ -272,6 +273,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
fieldGroupingResolver: null,
fieldGroupingResolverSequence: 0,
subsyncInProgress: false,
youtubePlaybackFlowPending: false,
initialArgs: null,
mpvSocketPath: values.mpvSocketPath,
texthookerPort: values.texthookerPort,
@@ -291,6 +293,7 @@ export function createAppState(values: AppStateInitialValues): AppState {
export function applyStartupState(appState: AppState, startupState: StartupState): void {
appState.initialArgs = startupState.initialArgs;
appState.youtubePlaybackFlowPending = Boolean(startupState.initialArgs.youtubePlay);
appState.mpvSocketPath = startupState.mpvSocketPath;
appState.texthookerPort = startupState.texthookerPort;
appState.backendOverride = startupState.backendOverride;