mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-25 00:11:26 -07:00
refactor: unify cli and runtime wiring for startup and youtube flow
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => {},
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -50,6 +50,7 @@ function createDeps() {
|
||||
}),
|
||||
runStatsCommand: async () => {},
|
||||
runJellyfinCommand: async () => {},
|
||||
runYoutubePlaybackFlow: async () => {},
|
||||
openYomitanSettings: () => {},
|
||||
cycleSecondarySubMode: () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => {},
|
||||
|
||||
@@ -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: () => {},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user