mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-03 06:22:41 -08:00
Overlay 2.0 (#12)
This commit is contained in:
@@ -48,6 +48,7 @@ export interface AppReadyRuntimeDepsFactoryInput {
|
||||
onCriticalConfigErrors?: AppReadyRuntimeDeps['onCriticalConfigErrors'];
|
||||
logDebug?: AppReadyRuntimeDeps['logDebug'];
|
||||
now?: AppReadyRuntimeDeps['now'];
|
||||
shouldSkipHeavyStartup?: AppReadyRuntimeDeps['shouldSkipHeavyStartup'];
|
||||
}
|
||||
|
||||
export function createAppLifecycleRuntimeDeps(
|
||||
@@ -103,6 +104,7 @@ export function createAppReadyRuntimeDeps(
|
||||
onCriticalConfigErrors: params.onCriticalConfigErrors,
|
||||
logDebug: params.logDebug,
|
||||
now: params.now,
|
||||
shouldSkipHeavyStartup: params.shouldSkipHeavyStartup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from './dependencies';
|
||||
|
||||
export interface CliCommandRuntimeServiceContext {
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getClient: CliCommandRuntimeServiceDepsParams['mpv']['getClient'];
|
||||
@@ -17,9 +18,7 @@ export interface CliCommandRuntimeServiceContext {
|
||||
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>;
|
||||
@@ -57,6 +56,7 @@ function createCliCommandDepsFromContext(
|
||||
context: CliCommandRuntimeServiceContext & CliCommandRuntimeServiceContextHandlers,
|
||||
): CliCommandRuntimeServiceDepsParams {
|
||||
return {
|
||||
setLogLevel: context.setLogLevel,
|
||||
mpv: {
|
||||
getSocketPath: context.getSocketPath,
|
||||
setSocketPath: context.setSocketPath,
|
||||
@@ -74,9 +74,7 @@ function createCliCommandDepsFromContext(
|
||||
isInitialized: context.isOverlayInitialized,
|
||||
initialize: context.initializeOverlay,
|
||||
toggleVisible: context.toggleVisibleOverlay,
|
||||
toggleInvisible: context.toggleInvisibleOverlay,
|
||||
setVisible: context.setVisibleOverlay,
|
||||
setInvisible: context.setInvisibleOverlay,
|
||||
},
|
||||
mining: {
|
||||
copyCurrentSubtitle: context.copyCurrentSubtitle,
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildConfigParseErrorDetails,
|
||||
buildConfigWarningDialogDetails,
|
||||
buildConfigWarningNotificationBody,
|
||||
buildConfigWarningSummary,
|
||||
failStartupFromConfig,
|
||||
@@ -53,6 +54,22 @@ test('buildConfigWarningNotificationBody includes concise warning details', () =
|
||||
);
|
||||
});
|
||||
|
||||
test('buildConfigWarningDialogDetails includes full warning details', () => {
|
||||
const details = buildConfigWarningDialogDetails('/tmp/config.jsonc', [
|
||||
{
|
||||
path: 'ankiConnect.pollingRate',
|
||||
message: 'must be >= 50',
|
||||
value: 10,
|
||||
fallback: 250,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.match(details, /SubMiner detected config validation issues\./);
|
||||
assert.match(details, /File: \/tmp\/config\.jsonc/);
|
||||
assert.match(details, /1\. ankiConnect\.pollingRate: must be >= 50/);
|
||||
assert.match(details, /actual=10 fallback=250/);
|
||||
});
|
||||
|
||||
test('buildConfigParseErrorDetails includes path error and restart guidance', () => {
|
||||
const details = buildConfigParseErrorDetails('/tmp/config.jsonc', 'unexpected token at line 1');
|
||||
|
||||
|
||||
@@ -61,6 +61,25 @@ export function buildConfigWarningNotificationBody(
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildConfigWarningDialogDetails(
|
||||
configPath: string,
|
||||
warnings: ConfigValidationWarning[],
|
||||
): string {
|
||||
const lines = warnings.map(
|
||||
(warning, index) =>
|
||||
`${index + 1}. ${warning.path}: ${warning.message} actual=${formatConfigValue(warning.value)} fallback=${formatConfigValue(warning.fallback)}`,
|
||||
);
|
||||
|
||||
return [
|
||||
'SubMiner detected config validation issues.',
|
||||
`File: ${configPath}`,
|
||||
'',
|
||||
...lines,
|
||||
'',
|
||||
'Defaults were applied where possible.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function buildConfigParseErrorDetails(configPath: string, parseError: string): string {
|
||||
return [
|
||||
'Failed to parse config file at:',
|
||||
|
||||
@@ -53,19 +53,18 @@ export function createSubsyncRuntimeDeps(params: SubsyncRuntimeDepsParams): Subs
|
||||
}
|
||||
|
||||
export interface MainIpcRuntimeServiceDepsParams {
|
||||
getInvisibleWindow: IpcDepsRuntimeOptions['getInvisibleWindow'];
|
||||
getMainWindow: IpcDepsRuntimeOptions['getMainWindow'];
|
||||
getVisibleOverlayVisibility: IpcDepsRuntimeOptions['getVisibleOverlayVisibility'];
|
||||
getInvisibleOverlayVisibility: IpcDepsRuntimeOptions['getInvisibleOverlayVisibility'];
|
||||
onOverlayModalClosed: IpcDepsRuntimeOptions['onOverlayModalClosed'];
|
||||
onOverlayModalOpened?: IpcDepsRuntimeOptions['onOverlayModalOpened'];
|
||||
openYomitanSettings: IpcDepsRuntimeOptions['openYomitanSettings'];
|
||||
quitApp: IpcDepsRuntimeOptions['quitApp'];
|
||||
toggleVisibleOverlay: IpcDepsRuntimeOptions['toggleVisibleOverlay'];
|
||||
tokenizeCurrentSubtitle: IpcDepsRuntimeOptions['tokenizeCurrentSubtitle'];
|
||||
getCurrentSubtitleRaw: IpcDepsRuntimeOptions['getCurrentSubtitleRaw'];
|
||||
getCurrentSubtitleAss: IpcDepsRuntimeOptions['getCurrentSubtitleAss'];
|
||||
getPlaybackPaused: IpcDepsRuntimeOptions['getPlaybackPaused'];
|
||||
focusMainWindow?: IpcDepsRuntimeOptions['focusMainWindow'];
|
||||
getMpvSubtitleRenderMetrics: IpcDepsRuntimeOptions['getMpvSubtitleRenderMetrics'];
|
||||
getSubtitlePosition: IpcDepsRuntimeOptions['getSubtitlePosition'];
|
||||
getSubtitleStyle: IpcDepsRuntimeOptions['getSubtitleStyle'];
|
||||
saveSubtitlePosition: IpcDepsRuntimeOptions['saveSubtitlePosition'];
|
||||
@@ -81,7 +80,6 @@ export interface MainIpcRuntimeServiceDepsParams {
|
||||
setRuntimeOption: IpcDepsRuntimeOptions['setRuntimeOption'];
|
||||
cycleRuntimeOption: IpcDepsRuntimeOptions['cycleRuntimeOption'];
|
||||
reportOverlayContentBounds: IpcDepsRuntimeOptions['reportOverlayContentBounds'];
|
||||
reportHoveredSubtitleToken: IpcDepsRuntimeOptions['reportHoveredSubtitleToken'];
|
||||
getAnilistStatus: IpcDepsRuntimeOptions['getAnilistStatus'];
|
||||
clearAnilistToken: IpcDepsRuntimeOptions['clearAnilistToken'];
|
||||
openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup'];
|
||||
@@ -115,6 +113,7 @@ export interface AnkiJimakuIpcRuntimeServiceDepsParams {
|
||||
}
|
||||
|
||||
export interface CliCommandRuntimeServiceDepsParams {
|
||||
setLogLevel?: CliCommandDepsRuntimeOptions['setLogLevel'];
|
||||
mpv: {
|
||||
getSocketPath: CliCommandDepsRuntimeOptions['mpv']['getSocketPath'];
|
||||
setSocketPath: CliCommandDepsRuntimeOptions['mpv']['setSocketPath'];
|
||||
@@ -132,9 +131,7 @@ export interface CliCommandRuntimeServiceDepsParams {
|
||||
isInitialized: CliCommandDepsRuntimeOptions['overlay']['isInitialized'];
|
||||
initialize: CliCommandDepsRuntimeOptions['overlay']['initialize'];
|
||||
toggleVisible: CliCommandDepsRuntimeOptions['overlay']['toggleVisible'];
|
||||
toggleInvisible: CliCommandDepsRuntimeOptions['overlay']['toggleInvisible'];
|
||||
setVisible: CliCommandDepsRuntimeOptions['overlay']['setVisible'];
|
||||
setInvisible: CliCommandDepsRuntimeOptions['overlay']['setInvisible'];
|
||||
};
|
||||
mining: {
|
||||
copyCurrentSubtitle: CliCommandDepsRuntimeOptions['mining']['copyCurrentSubtitle'];
|
||||
@@ -192,18 +189,17 @@ export function createMainIpcRuntimeServiceDeps(
|
||||
params: MainIpcRuntimeServiceDepsParams,
|
||||
): IpcDepsRuntimeOptions {
|
||||
return {
|
||||
getInvisibleWindow: params.getInvisibleWindow,
|
||||
getMainWindow: params.getMainWindow,
|
||||
getVisibleOverlayVisibility: params.getVisibleOverlayVisibility,
|
||||
getInvisibleOverlayVisibility: params.getInvisibleOverlayVisibility,
|
||||
onOverlayModalClosed: params.onOverlayModalClosed,
|
||||
onOverlayModalOpened: params.onOverlayModalOpened,
|
||||
openYomitanSettings: params.openYomitanSettings,
|
||||
quitApp: params.quitApp,
|
||||
toggleVisibleOverlay: params.toggleVisibleOverlay,
|
||||
tokenizeCurrentSubtitle: params.tokenizeCurrentSubtitle,
|
||||
getCurrentSubtitleRaw: params.getCurrentSubtitleRaw,
|
||||
getCurrentSubtitleAss: params.getCurrentSubtitleAss,
|
||||
getMpvSubtitleRenderMetrics: params.getMpvSubtitleRenderMetrics,
|
||||
getPlaybackPaused: params.getPlaybackPaused,
|
||||
getSubtitlePosition: params.getSubtitlePosition,
|
||||
getSubtitleStyle: params.getSubtitleStyle,
|
||||
saveSubtitlePosition: params.saveSubtitlePosition,
|
||||
@@ -220,7 +216,6 @@ export function createMainIpcRuntimeServiceDeps(
|
||||
setRuntimeOption: params.setRuntimeOption,
|
||||
cycleRuntimeOption: params.cycleRuntimeOption,
|
||||
reportOverlayContentBounds: params.reportOverlayContentBounds,
|
||||
reportHoveredSubtitleToken: params.reportHoveredSubtitleToken,
|
||||
getAnilistStatus: params.getAnilistStatus,
|
||||
clearAnilistToken: params.clearAnilistToken,
|
||||
openAnilistSetup: params.openAnilistSetup,
|
||||
@@ -262,6 +257,7 @@ export function createCliCommandRuntimeServiceDeps(
|
||||
params: CliCommandRuntimeServiceDepsParams,
|
||||
): CliCommandDepsRuntimeOptions {
|
||||
return {
|
||||
setLogLevel: params.setLogLevel,
|
||||
mpv: {
|
||||
getSocketPath: params.mpv.getSocketPath,
|
||||
setSocketPath: params.mpv.setSocketPath,
|
||||
@@ -279,9 +275,7 @@ export function createCliCommandRuntimeServiceDeps(
|
||||
isInitialized: params.overlay.isInitialized,
|
||||
initialize: params.overlay.initialize,
|
||||
toggleVisible: params.overlay.toggleVisible,
|
||||
toggleInvisible: params.overlay.toggleInvisible,
|
||||
setVisible: params.overlay.setVisible,
|
||||
setInvisible: params.overlay.setInvisible,
|
||||
},
|
||||
mining: {
|
||||
copyCurrentSubtitle: params.mining.copyCurrentSubtitle,
|
||||
|
||||
@@ -28,7 +28,7 @@ export function getFrequencyDictionarySearchPaths(
|
||||
|
||||
const rawSearchPaths: string[] = [];
|
||||
// User-provided path takes precedence over bundled/default roots.
|
||||
// Root list should include `vendor/jiten_freq_global` in callers.
|
||||
// Root list should include default installed frequency-dictionary locations in callers.
|
||||
if (sourcePath && sourcePath.trim()) {
|
||||
rawSearchPaths.push(sourcePath.trim());
|
||||
rawSearchPaths.push(path.join(sourcePath.trim(), 'frequency-dictionary'));
|
||||
|
||||
@@ -12,6 +12,7 @@ type MockWindow = {
|
||||
hideCount: number;
|
||||
sent: unknown[][];
|
||||
loading: boolean;
|
||||
url: string;
|
||||
loadCallbacks: Array<() => void>;
|
||||
};
|
||||
|
||||
@@ -19,7 +20,8 @@ function createMockWindow(): MockWindow & {
|
||||
isDestroyed: () => boolean;
|
||||
isVisible: () => boolean;
|
||||
isFocused: () => boolean;
|
||||
setIgnoreMouseEvents: (ignore: boolean) => void;
|
||||
getURL: () => string;
|
||||
setIgnoreMouseEvents: (ignore: boolean, options?: { forward?: boolean }) => void;
|
||||
getShowCount: () => number;
|
||||
getHideCount: () => number;
|
||||
show: () => void;
|
||||
@@ -28,6 +30,7 @@ function createMockWindow(): MockWindow & {
|
||||
webContents: {
|
||||
focused: boolean;
|
||||
isLoading: () => boolean;
|
||||
getURL: () => string;
|
||||
send: (channel: string, payload?: unknown) => void;
|
||||
isFocused: () => boolean;
|
||||
once: (event: 'did-finish-load', cb: () => void) => void;
|
||||
@@ -44,14 +47,16 @@ function createMockWindow(): MockWindow & {
|
||||
hideCount: 0,
|
||||
sent: [],
|
||||
loading: false,
|
||||
url: 'file:///overlay/index.html?layer=modal',
|
||||
loadCallbacks: [],
|
||||
};
|
||||
return {
|
||||
const window = {
|
||||
...state,
|
||||
isDestroyed: () => state.destroyed,
|
||||
isVisible: () => state.visible,
|
||||
isFocused: () => state.focused,
|
||||
setIgnoreMouseEvents: (ignore: boolean) => {
|
||||
getURL: () => state.url,
|
||||
setIgnoreMouseEvents: (ignore: boolean, _options?: { forward?: boolean }) => {
|
||||
state.ignoreMouseEvents = ignore;
|
||||
},
|
||||
getShowCount: () => state.showCount,
|
||||
@@ -69,7 +74,8 @@ function createMockWindow(): MockWindow & {
|
||||
},
|
||||
webContents: {
|
||||
isLoading: () => state.loading,
|
||||
send: (channel, payload) => {
|
||||
getURL: () => state.url,
|
||||
send: (channel: string, payload?: unknown) => {
|
||||
if (payload === undefined) {
|
||||
state.sent.push([channel]);
|
||||
return;
|
||||
@@ -78,7 +84,7 @@ function createMockWindow(): MockWindow & {
|
||||
},
|
||||
focused: false,
|
||||
isFocused: () => state.webContentsFocused,
|
||||
once: (_event, cb) => {
|
||||
once: (_event: 'did-finish-load', cb: () => void) => {
|
||||
state.loadCallbacks.push(cb);
|
||||
},
|
||||
focus: () => {
|
||||
@@ -86,6 +92,29 @@ function createMockWindow(): MockWindow & {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'loading', {
|
||||
get: () => state.loading,
|
||||
set: (value: boolean) => {
|
||||
state.loading = value;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'url', {
|
||||
get: () => state.url,
|
||||
set: (value: string) => {
|
||||
state.url = value;
|
||||
},
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'ignoreMouseEvents', {
|
||||
get: () => state.ignoreMouseEvents,
|
||||
set: (value: boolean) => {
|
||||
state.ignoreMouseEvents = value;
|
||||
},
|
||||
});
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
test('sendToActiveOverlayWindow targets modal window with full geometry and tracks close restore', () => {
|
||||
@@ -93,7 +122,6 @@ test('sendToActiveOverlayWindow targets modal window with full geometry and trac
|
||||
const calls: string[] = [];
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => {
|
||||
calls.push('create-modal-window');
|
||||
@@ -111,6 +139,8 @@ test('sendToActiveOverlayWindow targets modal window with full geometry and trac
|
||||
assert.equal(sent, true);
|
||||
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().has('runtime-options'), true);
|
||||
assert.deepEqual(calls, ['bounds:10,20,300,200']);
|
||||
assert.equal(window.getShowCount(), 0);
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
assert.equal(window.getShowCount(), 1);
|
||||
assert.equal(window.isFocused(), true);
|
||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||
@@ -121,7 +151,6 @@ test('sendToActiveOverlayWindow creates modal window lazily when absent', () =>
|
||||
let modalWindow: ReturnType<typeof createMockWindow> | null = null;
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => {
|
||||
modalWindow = window;
|
||||
@@ -135,14 +164,47 @@ test('sendToActiveOverlayWindow creates modal window lazily when absent', () =>
|
||||
runtime.sendToActiveOverlayWindow('jimaku:open', undefined, { restoreOnModalClose: 'jimaku' }),
|
||||
true,
|
||||
);
|
||||
assert.equal(window.getShowCount(), 0);
|
||||
runtime.notifyOverlayModalOpened('jimaku');
|
||||
assert.equal(window.getShowCount(), 1);
|
||||
assert.deepEqual(window.sent, [['jimaku:open']]);
|
||||
});
|
||||
|
||||
test('sendToActiveOverlayWindow waits for blank modal URL before sending open command', () => {
|
||||
const window = createMockWindow();
|
||||
window.url = '';
|
||||
window.loading = true;
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => {
|
||||
throw new Error('modal window should not be created when already present');
|
||||
},
|
||||
getModalGeometry: () => ({ x: 10, y: 20, width: 300, height: 200 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.deepEqual(window.sent, []);
|
||||
|
||||
assert.equal(window.loadCallbacks.length, 1);
|
||||
window.loading = false;
|
||||
window.url = 'file:///overlay/index.html?layer=modal';
|
||||
window.loadCallbacks[0]!();
|
||||
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
assert.deepEqual(window.sent, [['runtime-options:open']]);
|
||||
assert.equal(window.getShowCount(), 1);
|
||||
});
|
||||
|
||||
test('handleOverlayModalClosed hides modal window only after all pending modals close', () => {
|
||||
const window = createMockWindow();
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
@@ -152,9 +214,13 @@ test('handleOverlayModalClosed hides modal window only after all pending modals
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||
restoreOnModalClose: 'subsync',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow(
|
||||
'subsync:open-manual',
|
||||
{ sourceTracks: [] },
|
||||
{
|
||||
restoreOnModalClose: 'subsync',
|
||||
},
|
||||
);
|
||||
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
assert.equal(window.getHideCount(), 0);
|
||||
@@ -163,13 +229,33 @@ test('handleOverlayModalClosed hides modal window only after all pending modals
|
||||
assert.equal(window.getHideCount(), 1);
|
||||
});
|
||||
|
||||
test('sendToActiveOverlayWindow prefers visible main overlay window for modal open', () => {
|
||||
const mainWindow = createMockWindow();
|
||||
mainWindow.visible = true;
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => mainWindow as never,
|
||||
getModalWindow: () => null,
|
||||
createModalWindow: () => {
|
||||
throw new Error('modal window should not be created when main overlay is visible');
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.deepEqual(mainWindow.sent, [['runtime-options:open']]);
|
||||
});
|
||||
|
||||
test('modal runtime notifies callers when modal input state becomes active/inactive', () => {
|
||||
const window = createMockWindow();
|
||||
const state: boolean[] = [];
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
@@ -185,9 +271,15 @@ test('modal runtime notifies callers when modal input state becomes active/inact
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow('subsync:open-manual', { sourceTracks: [] }, {
|
||||
restoreOnModalClose: 'subsync',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow(
|
||||
'subsync:open-manual',
|
||||
{ sourceTracks: [] },
|
||||
{
|
||||
restoreOnModalClose: 'subsync',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(state, []);
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
assert.deepEqual(state, [true]);
|
||||
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
@@ -197,22 +289,119 @@ test('modal runtime notifies callers when modal input state becomes active/inact
|
||||
assert.deepEqual(state, [true, false]);
|
||||
});
|
||||
|
||||
test('notifyOverlayModalOpened enables input on visible main overlay window when no modal window exists', () => {
|
||||
const mainWindow = createMockWindow();
|
||||
mainWindow.visible = true;
|
||||
mainWindow.ignoreMouseEvents = true;
|
||||
const state: boolean[] = [];
|
||||
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => mainWindow as never,
|
||||
getModalWindow: () => null,
|
||||
createModalWindow: () => {
|
||||
throw new Error('modal window should not be created when main overlay is visible');
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
},
|
||||
{
|
||||
onModalStateChange: (active: boolean): void => {
|
||||
state.push(active);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.equal(state, [true]);
|
||||
assert.equal(mainWindow.ignoreMouseEvents, false);
|
||||
assert.equal(mainWindow.isFocused(), true);
|
||||
assert.equal(mainWindow.webContentsFocused, true);
|
||||
});
|
||||
|
||||
test('handleOverlayModalClosed resets modal state even when modal window does not exist', () => {
|
||||
const state: boolean[] = [];
|
||||
const runtime = createOverlayModalRuntimeService(
|
||||
{
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => null,
|
||||
createModalWindow: () => null,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
},
|
||||
{
|
||||
onModalStateChange: (active: boolean): void => {
|
||||
state.push(active);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
runtime.sendToActiveOverlayWindow('runtime-options:open', undefined, {
|
||||
restoreOnModalClose: 'runtime-options',
|
||||
});
|
||||
runtime.notifyOverlayModalOpened('runtime-options');
|
||||
runtime.handleOverlayModalClosed('runtime-options');
|
||||
|
||||
assert.deepEqual(state, [true, false]);
|
||||
});
|
||||
|
||||
test('handleOverlayModalClosed hides modal window for single kiku modal', () => {
|
||||
const window = createMockWindow();
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getInvisibleWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => window as never,
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
runtime.sendToActiveOverlayWindow('kiku:field-grouping-open', { test: true }, {
|
||||
restoreOnModalClose: 'kiku',
|
||||
});
|
||||
runtime.sendToActiveOverlayWindow(
|
||||
'kiku:field-grouping-open',
|
||||
{ test: true },
|
||||
{
|
||||
restoreOnModalClose: 'kiku',
|
||||
},
|
||||
);
|
||||
runtime.handleOverlayModalClosed('kiku');
|
||||
|
||||
assert.equal(window.getHideCount(), 1);
|
||||
assert.equal(runtime.getRestoreVisibleOverlayOnModalClose().size, 0);
|
||||
});
|
||||
|
||||
test('modal fallback reveal keeps mouse events ignored until modal confirms open', async () => {
|
||||
const window = createMockWindow();
|
||||
const runtime = createOverlayModalRuntimeService({
|
||||
getMainWindow: () => null,
|
||||
getModalWindow: () => window as never,
|
||||
createModalWindow: () => {
|
||||
throw new Error('modal window should not be created when already present');
|
||||
},
|
||||
getModalGeometry: () => ({ x: 0, y: 0, width: 400, height: 300 }),
|
||||
setModalWindowBounds: () => {},
|
||||
});
|
||||
|
||||
window.loading = true;
|
||||
window.url = '';
|
||||
|
||||
const sent = runtime.sendToActiveOverlayWindow('jimaku:open', undefined, {
|
||||
restoreOnModalClose: 'jimaku',
|
||||
});
|
||||
|
||||
assert.equal(sent, true);
|
||||
assert.equal(window.ignoreMouseEvents, false);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 260);
|
||||
});
|
||||
|
||||
assert.equal(window.getShowCount(), 1);
|
||||
assert.equal(window.ignoreMouseEvents, true);
|
||||
|
||||
runtime.notifyOverlayModalOpened('jimaku');
|
||||
assert.equal(window.ignoreMouseEvents, false);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { WindowGeometry } from '../types';
|
||||
|
||||
const MODAL_REVEAL_FALLBACK_DELAY_MS = 250;
|
||||
|
||||
type OverlayHostedModal = 'runtime-options' | 'subsync' | 'jimaku' | 'kiku';
|
||||
type OverlayHostLayer = 'visible' | 'invisible';
|
||||
|
||||
export interface OverlayWindowResolver {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getModalWindow: () => BrowserWindow | null;
|
||||
createModalWindow: () => BrowserWindow | null;
|
||||
getModalGeometry: () => WindowGeometry;
|
||||
@@ -21,6 +21,7 @@ export interface OverlayModalRuntime {
|
||||
) => boolean;
|
||||
openRuntimeOptionsPalette: () => void;
|
||||
handleOverlayModalClosed: (modal: OverlayHostedModal) => void;
|
||||
notifyOverlayModalOpened: (modal: OverlayHostedModal) => void;
|
||||
getRestoreVisibleOverlayOnModalClose: () => Set<OverlayHostedModal>;
|
||||
}
|
||||
|
||||
@@ -34,6 +35,8 @@ export function createOverlayModalRuntimeService(
|
||||
): OverlayModalRuntime {
|
||||
const restoreVisibleOverlayOnModalClose = new Set<OverlayHostedModal>();
|
||||
let modalActive = false;
|
||||
let pendingModalWindowReveal: BrowserWindow | null = null;
|
||||
let pendingModalWindowRevealTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const notifyModalStateChange = (nextState: boolean): void => {
|
||||
if (modalActive === nextState) return;
|
||||
@@ -53,44 +56,130 @@ export function createOverlayModalRuntimeService(
|
||||
return createdWindow;
|
||||
};
|
||||
|
||||
const getTargetOverlayWindow = (): {
|
||||
window: BrowserWindow;
|
||||
layer: OverlayHostLayer;
|
||||
} | null => {
|
||||
const getTargetOverlayWindow = (): BrowserWindow | null => {
|
||||
const visibleMainWindow = deps.getMainWindow();
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
|
||||
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
|
||||
return { window: visibleMainWindow, layer: 'visible' };
|
||||
return visibleMainWindow;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getActiveOverlayWindowForModalInput = (): BrowserWindow | null => {
|
||||
const modalWindow = deps.getModalWindow();
|
||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||
return modalWindow;
|
||||
}
|
||||
|
||||
if (invisibleWindow && !invisibleWindow.isDestroyed()) {
|
||||
return { window: invisibleWindow, layer: 'invisible' };
|
||||
const visibleMainWindow = deps.getMainWindow();
|
||||
if (visibleMainWindow && !visibleMainWindow.isDestroyed()) {
|
||||
return visibleMainWindow;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const showModalWindow = (window: BrowserWindow): void => {
|
||||
window.show();
|
||||
window.setIgnoreMouseEvents(false);
|
||||
const isWindowReadyForIpc = (window: BrowserWindow): boolean => {
|
||||
if (window.webContents.isLoading()) {
|
||||
return false;
|
||||
}
|
||||
const currentURL = window.webContents.getURL();
|
||||
return currentURL !== '' && currentURL !== 'about:blank';
|
||||
};
|
||||
|
||||
const elevateModalWindow = (window: BrowserWindow): void => {
|
||||
if (window.isDestroyed()) return;
|
||||
window.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||
window.moveTop();
|
||||
};
|
||||
|
||||
const sendOrQueueForWindow = (
|
||||
window: BrowserWindow,
|
||||
sendNow: (window: BrowserWindow) => void,
|
||||
): void => {
|
||||
if (isWindowReadyForIpc(window)) {
|
||||
sendNow(window);
|
||||
return;
|
||||
}
|
||||
|
||||
window.webContents.once('did-finish-load', () => {
|
||||
if (!window.isDestroyed() && !window.webContents.isLoading()) {
|
||||
sendNow(window);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showModalWindow = (
|
||||
window: BrowserWindow,
|
||||
options: {
|
||||
passThroughMouseEvents: boolean;
|
||||
} = { passThroughMouseEvents: false },
|
||||
): void => {
|
||||
if (!window.isVisible()) {
|
||||
window.show();
|
||||
}
|
||||
elevateModalWindow(window);
|
||||
if (options.passThroughMouseEvents) {
|
||||
window.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
window.setIgnoreMouseEvents(false);
|
||||
}
|
||||
window.focus();
|
||||
if (!window.webContents.isFocused()) {
|
||||
window.webContents.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const showOverlayWindowForModal = (window: BrowserWindow, layer: OverlayHostLayer): void => {
|
||||
if (layer === 'invisible' && typeof window.showInactive === 'function') {
|
||||
window.showInactive();
|
||||
} else {
|
||||
window.show();
|
||||
const ensureModalWindowInteractive = (window: BrowserWindow): void => {
|
||||
if (window.isVisible()) {
|
||||
window.setIgnoreMouseEvents(false);
|
||||
if (!window.isFocused()) {
|
||||
window.focus();
|
||||
}
|
||||
if (!window.webContents.isFocused()) {
|
||||
window.webContents.focus();
|
||||
}
|
||||
elevateModalWindow(window);
|
||||
return;
|
||||
}
|
||||
|
||||
showModalWindow(window);
|
||||
};
|
||||
|
||||
const showOverlayWindowForModal = (window: BrowserWindow): void => {
|
||||
window.show();
|
||||
if (!window.isFocused()) {
|
||||
window.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const clearPendingModalWindowReveal = (): void => {
|
||||
if (pendingModalWindowRevealTimeout === null) {
|
||||
pendingModalWindowReveal = null;
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(pendingModalWindowRevealTimeout);
|
||||
pendingModalWindowRevealTimeout = null;
|
||||
pendingModalWindowReveal = null;
|
||||
};
|
||||
|
||||
const scheduleModalWindowReveal = (window: BrowserWindow): void => {
|
||||
pendingModalWindowReveal = window;
|
||||
if (pendingModalWindowRevealTimeout !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingModalWindowRevealTimeout = setTimeout(() => {
|
||||
const targetWindow = pendingModalWindowReveal;
|
||||
clearPendingModalWindowReveal();
|
||||
if (!targetWindow || targetWindow.isDestroyed() || targetWindow.isVisible()) {
|
||||
return;
|
||||
}
|
||||
showModalWindow(targetWindow, { passThroughMouseEvents: false });
|
||||
}, MODAL_REVEAL_FALLBACK_DELAY_MS);
|
||||
};
|
||||
|
||||
const sendToActiveOverlayWindow = (
|
||||
channel: string,
|
||||
payload?: unknown,
|
||||
@@ -99,6 +188,7 @@ export function createOverlayModalRuntimeService(
|
||||
const restoreOnModalClose = runtimeOptions?.restoreOnModalClose;
|
||||
|
||||
const sendNow = (window: BrowserWindow): void => {
|
||||
ensureModalWindowInteractive(window);
|
||||
if (payload === undefined) {
|
||||
window.webContents.send(channel);
|
||||
} else {
|
||||
@@ -107,55 +197,43 @@ export function createOverlayModalRuntimeService(
|
||||
};
|
||||
|
||||
if (restoreOnModalClose) {
|
||||
const modalWindow = resolveModalWindow();
|
||||
if (!modalWindow) return false;
|
||||
|
||||
deps.setModalWindowBounds(deps.getModalGeometry());
|
||||
const wasVisible = modalWindow.isVisible();
|
||||
const wasModalActive = restoreVisibleOverlayOnModalClose.size > 0;
|
||||
restoreVisibleOverlayOnModalClose.add(restoreOnModalClose);
|
||||
if (!wasModalActive) {
|
||||
notifyModalStateChange(true);
|
||||
}
|
||||
|
||||
if (!wasVisible) {
|
||||
showModalWindow(modalWindow);
|
||||
} else if (!modalWindow.isFocused()) {
|
||||
showModalWindow(modalWindow);
|
||||
}
|
||||
|
||||
if (modalWindow.webContents.isLoading()) {
|
||||
modalWindow.webContents.once('did-finish-load', () => {
|
||||
if (modalWindow && !modalWindow.isDestroyed() && !modalWindow.webContents.isLoading()) {
|
||||
sendNow(modalWindow);
|
||||
const mainWindow = getTargetOverlayWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible()) {
|
||||
sendOrQueueForWindow(mainWindow, (window) => {
|
||||
if (payload === undefined) {
|
||||
window.webContents.send(channel);
|
||||
} else {
|
||||
window.webContents.send(channel, payload);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
sendNow(modalWindow);
|
||||
const modalWindow = resolveModalWindow();
|
||||
if (!modalWindow) return false;
|
||||
|
||||
deps.setModalWindowBounds(deps.getModalGeometry());
|
||||
const wasVisible = modalWindow.isVisible();
|
||||
if (!wasVisible) {
|
||||
scheduleModalWindowReveal(modalWindow);
|
||||
} else if (!modalWindow.isFocused()) {
|
||||
showModalWindow(modalWindow);
|
||||
}
|
||||
|
||||
sendOrQueueForWindow(modalWindow, sendNow);
|
||||
return true;
|
||||
}
|
||||
|
||||
const target = getTargetOverlayWindow();
|
||||
if (!target) return false;
|
||||
|
||||
const { window: targetWindow, layer } = target;
|
||||
const wasVisible = targetWindow.isVisible();
|
||||
const wasVisible = target.isVisible();
|
||||
if (!wasVisible) {
|
||||
showOverlayWindowForModal(targetWindow, layer);
|
||||
showOverlayWindowForModal(target);
|
||||
}
|
||||
|
||||
if (targetWindow.webContents.isLoading()) {
|
||||
targetWindow.webContents.once('did-finish-load', () => {
|
||||
if (targetWindow && !targetWindow.isDestroyed() && !targetWindow.webContents.isLoading()) {
|
||||
sendNow(targetWindow);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
sendNow(targetWindow);
|
||||
sendOrQueueForWindow(target, sendNow);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -169,19 +247,44 @@ export function createOverlayModalRuntimeService(
|
||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||
restoreVisibleOverlayOnModalClose.delete(modal);
|
||||
const modalWindow = deps.getModalWindow();
|
||||
if (!modalWindow || modalWindow.isDestroyed()) return;
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
clearPendingModalWindowReveal();
|
||||
notifyModalStateChange(false);
|
||||
if (modalWindow && !modalWindow.isDestroyed()) {
|
||||
modalWindow.hide();
|
||||
}
|
||||
}
|
||||
if (restoreVisibleOverlayOnModalClose.size === 0) {
|
||||
modalWindow.hide();
|
||||
};
|
||||
|
||||
const notifyOverlayModalOpened = (modal: OverlayHostedModal): void => {
|
||||
if (!restoreVisibleOverlayOnModalClose.has(modal)) return;
|
||||
notifyModalStateChange(true);
|
||||
const targetWindow = getActiveOverlayWindowForModalInput();
|
||||
clearPendingModalWindowReveal();
|
||||
if (!targetWindow || targetWindow.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetWindow.isVisible()) {
|
||||
targetWindow.setIgnoreMouseEvents(false);
|
||||
elevateModalWindow(targetWindow);
|
||||
if (!targetWindow.isFocused()) {
|
||||
targetWindow.focus();
|
||||
}
|
||||
if (!targetWindow.webContents.isFocused()) {
|
||||
targetWindow.webContents.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showModalWindow(targetWindow);
|
||||
};
|
||||
|
||||
return {
|
||||
sendToActiveOverlayWindow,
|
||||
openRuntimeOptionsPalette,
|
||||
handleOverlayModalClosed,
|
||||
notifyOverlayModalOpened,
|
||||
getRestoreVisibleOverlayOnModalClose: () => restoreVisibleOverlayOnModalClose,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,50 +2,31 @@ import type { BrowserWindow } from 'electron';
|
||||
|
||||
import type { BaseWindowTracker } from '../window-trackers';
|
||||
import type { WindowGeometry } from '../types';
|
||||
import {
|
||||
syncInvisibleOverlayMousePassthrough,
|
||||
updateInvisibleOverlayVisibility,
|
||||
updateVisibleOverlayVisibility,
|
||||
} from '../core/services';
|
||||
import { updateVisibleOverlayVisibility } from '../core/services';
|
||||
|
||||
export interface OverlayVisibilityRuntimeDeps {
|
||||
getMainWindow: () => BrowserWindow | null;
|
||||
getInvisibleWindow: () => BrowserWindow | null;
|
||||
getVisibleOverlayVisible: () => boolean;
|
||||
getInvisibleOverlayVisible: () => boolean;
|
||||
getWindowTracker: () => BaseWindowTracker | null;
|
||||
getTrackerNotReadyWarningShown: () => boolean;
|
||||
setTrackerNotReadyWarningShown: (shown: boolean) => void;
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
|
||||
syncPrimaryOverlayWindowLayer: (layer: 'visible') => void;
|
||||
enforceOverlayLayerOrder: () => void;
|
||||
syncOverlayShortcuts: () => void;
|
||||
isMacOSPlatform: () => boolean;
|
||||
showOverlayLoadingOsd: (message: string) => void;
|
||||
resolveFallbackBounds: () => WindowGeometry;
|
||||
}
|
||||
|
||||
export interface OverlayVisibilityRuntimeService {
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncInvisibleOverlayMousePassthrough: () => void;
|
||||
}
|
||||
|
||||
export function createOverlayVisibilityRuntimeService(
|
||||
deps: OverlayVisibilityRuntimeDeps,
|
||||
): OverlayVisibilityRuntimeService {
|
||||
const hasInvisibleWindow = (): boolean => {
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
return Boolean(invisibleWindow && !invisibleWindow.isDestroyed());
|
||||
};
|
||||
|
||||
const setIgnoreMouseEvents = (
|
||||
ignore: boolean,
|
||||
options?: Parameters<BrowserWindow['setIgnoreMouseEvents']>[1],
|
||||
): void => {
|
||||
const invisibleWindow = deps.getInvisibleWindow();
|
||||
if (!invisibleWindow || invisibleWindow.isDestroyed()) return;
|
||||
invisibleWindow.setIgnoreMouseEvents(ignore, options);
|
||||
};
|
||||
|
||||
return {
|
||||
updateVisibleOverlayVisibility(): void {
|
||||
updateVisibleOverlayVisibility({
|
||||
@@ -59,31 +40,13 @@ export function createOverlayVisibilityRuntimeService(
|
||||
updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateVisibleOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||
syncPrimaryOverlayWindowLayer: (layer: 'visible') =>
|
||||
deps.syncPrimaryOverlayWindowLayer(layer),
|
||||
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
|
||||
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
|
||||
});
|
||||
},
|
||||
|
||||
updateInvisibleOverlayVisibility(): void {
|
||||
updateInvisibleOverlayVisibility({
|
||||
invisibleWindow: deps.getInvisibleWindow(),
|
||||
visibleOverlayVisible: deps.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
|
||||
windowTracker: deps.getWindowTracker(),
|
||||
updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
|
||||
deps.updateInvisibleOverlayBounds(geometry),
|
||||
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),
|
||||
enforceOverlayLayerOrder: () => deps.enforceOverlayLayerOrder(),
|
||||
syncOverlayShortcuts: () => deps.syncOverlayShortcuts(),
|
||||
});
|
||||
},
|
||||
|
||||
syncInvisibleOverlayMousePassthrough(): void {
|
||||
syncInvisibleOverlayMousePassthrough({
|
||||
hasInvisibleWindow,
|
||||
setIgnoreMouseEvents,
|
||||
visibleOverlayVisible: deps.getVisibleOverlayVisible(),
|
||||
invisibleOverlayVisible: deps.getInvisibleOverlayVisible(),
|
||||
isMacOSPlatform: deps.isMacOSPlatform(),
|
||||
showOverlayLoadingOsd: (message: string) => deps.showOverlayLoadingOsd(message),
|
||||
resolveFallbackBounds: () => deps.resolveFallbackBounds(),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@ import type {
|
||||
createMaybeProbeAnilistDurationHandler,
|
||||
} from './anilist-media-guess';
|
||||
|
||||
type MaybeProbeAnilistDurationMainDeps = Parameters<typeof createMaybeProbeAnilistDurationHandler>[0];
|
||||
type MaybeProbeAnilistDurationMainDeps = Parameters<
|
||||
typeof createMaybeProbeAnilistDurationHandler
|
||||
>[0];
|
||||
type EnsureAnilistMediaGuessMainDeps = Parameters<typeof createEnsureAnilistMediaGuessHandler>[0];
|
||||
|
||||
export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
|
||||
@@ -19,13 +21,17 @@ export function createBuildMaybeProbeAnilistDurationMainDepsHandler(
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildEnsureAnilistMediaGuessMainDepsHandler(deps: EnsureAnilistMediaGuessMainDeps) {
|
||||
export function createBuildEnsureAnilistMediaGuessMainDepsHandler(
|
||||
deps: EnsureAnilistMediaGuessMainDeps,
|
||||
) {
|
||||
return (): EnsureAnilistMediaGuessMainDeps => ({
|
||||
getState: () => deps.getState(),
|
||||
setState: (state) => deps.setState(state),
|
||||
resolveMediaPathForJimaku: (currentMediaPath) => deps.resolveMediaPathForJimaku(currentMediaPath),
|
||||
resolveMediaPathForJimaku: (currentMediaPath) =>
|
||||
deps.resolveMediaPathForJimaku(currentMediaPath),
|
||||
getCurrentMediaPath: () => deps.getCurrentMediaPath(),
|
||||
getCurrentMediaTitle: () => deps.getCurrentMediaTitle(),
|
||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) => deps.guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) =>
|
||||
deps.guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ import type {
|
||||
createSetAnilistMediaGuessRuntimeStateHandler,
|
||||
} from './anilist-media-state';
|
||||
|
||||
type GetCurrentAnilistMediaKeyMainDeps = Parameters<typeof createGetCurrentAnilistMediaKeyHandler>[0];
|
||||
type ResetAnilistMediaTrackingMainDeps = Parameters<typeof createResetAnilistMediaTrackingHandler>[0];
|
||||
type GetCurrentAnilistMediaKeyMainDeps = Parameters<
|
||||
typeof createGetCurrentAnilistMediaKeyHandler
|
||||
>[0];
|
||||
type ResetAnilistMediaTrackingMainDeps = Parameters<
|
||||
typeof createResetAnilistMediaTrackingHandler
|
||||
>[0];
|
||||
type GetAnilistMediaGuessRuntimeStateMainDeps = Parameters<
|
||||
typeof createGetAnilistMediaGuessRuntimeStateHandler
|
||||
>[0];
|
||||
|
||||
@@ -47,7 +47,8 @@ export function createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
|
||||
hasAttemptedUpdateKey: (key: string) => deps.hasAttemptedUpdateKey(key),
|
||||
processNextAnilistRetryUpdate: () => deps.processNextAnilistRetryUpdate(),
|
||||
refreshAnilistClientSecretState: () => deps.refreshAnilistClientSecretState(),
|
||||
enqueueRetry: (key: string, title: string, episode: number) => deps.enqueueRetry(key, title, episode),
|
||||
enqueueRetry: (key: string, title: string, episode: number) =>
|
||||
deps.enqueueRetry(key, title, episode),
|
||||
markRetryFailure: (key: string, message: string) => deps.markRetryFailure(key, message),
|
||||
markRetrySuccess: (key: string) => deps.markRetrySuccess(key),
|
||||
refreshRetryQueueState: () => deps.refreshRetryQueueState(),
|
||||
|
||||
@@ -64,7 +64,11 @@ export function createProcessNextAnilistRetryUpdateHandler(deps: {
|
||||
return { ok: false, message: 'AniList token unavailable for queued retry.' };
|
||||
}
|
||||
|
||||
const result = await deps.updateAnilistPostWatchProgress(accessToken, queued.title, queued.episode);
|
||||
const result = await deps.updateAnilistPostWatchProgress(
|
||||
accessToken,
|
||||
queued.title,
|
||||
queued.episode,
|
||||
);
|
||||
if (result.status === 'updated' || result.status === 'skipped') {
|
||||
deps.markSuccess(queued.key);
|
||||
deps.rememberAttemptedUpdateKey(queued.key);
|
||||
@@ -166,7 +170,11 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deps.updateAnilistPostWatchProgress(accessToken, guess.title, guess.episode);
|
||||
const result = await deps.updateAnilistPostWatchProgress(
|
||||
accessToken,
|
||||
guess.title,
|
||||
guess.episode,
|
||||
);
|
||||
if (result.status === 'updated') {
|
||||
deps.rememberAttemptedUpdateKey(attemptKey);
|
||||
deps.markRetrySuccess(attemptKey);
|
||||
|
||||
@@ -44,7 +44,8 @@ export function createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(
|
||||
deps: HandleAnilistSetupProtocolUrlMainDeps,
|
||||
) {
|
||||
return (): HandleAnilistSetupProtocolUrlMainDeps => ({
|
||||
consumeAnilistSetupTokenFromUrl: (rawUrl: string) => deps.consumeAnilistSetupTokenFromUrl(rawUrl),
|
||||
consumeAnilistSetupTokenFromUrl: (rawUrl: string) =>
|
||||
deps.consumeAnilistSetupTokenFromUrl(rawUrl),
|
||||
logWarn: (message: string, details: unknown) => deps.logWarn(message, details),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,11 +66,7 @@ export function createRegisterSubminerProtocolClientHandler(deps: {
|
||||
getArgv: () => string[];
|
||||
execPath: string;
|
||||
resolvePath: (value: string) => string;
|
||||
setAsDefaultProtocolClient: (
|
||||
scheme: string,
|
||||
path?: string,
|
||||
args?: string[],
|
||||
) => boolean;
|
||||
setAsDefaultProtocolClient: (scheme: string, path?: string, args?: string[]) => boolean;
|
||||
logWarn: (message: string, details?: unknown) => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
|
||||
@@ -259,7 +259,8 @@ test('open anilist setup handler no-ops when existing setup window focused', ()
|
||||
|
||||
test('open anilist setup handler wires navigation, fallback, and lifecycle', () => {
|
||||
let openHandler: ((params: { url: string }) => { action: 'deny' }) | null = null;
|
||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null;
|
||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
|
||||
null;
|
||||
let didNavigateHandler: ((event: unknown, url: string) => void) | null = null;
|
||||
let didFinishLoadHandler: (() => void) | null = null;
|
||||
let didFailLoadHandler:
|
||||
@@ -276,7 +277,12 @@ test('open anilist setup handler wires navigation, fallback, and lifecycle', ()
|
||||
openHandler = handler;
|
||||
},
|
||||
on: (
|
||||
event: 'will-navigate' | 'will-redirect' | 'did-navigate' | 'did-fail-load' | 'did-finish-load',
|
||||
event:
|
||||
| 'will-navigate'
|
||||
| 'will-redirect'
|
||||
| 'did-navigate'
|
||||
| 'did-fail-load'
|
||||
| 'did-finish-load',
|
||||
handler: (...args: any[]) => void,
|
||||
) => {
|
||||
if (event === 'will-navigate') willNavigateHandler = handler as never;
|
||||
|
||||
@@ -126,7 +126,11 @@ export function createAnilistSetupDidNavigateHandler(deps: {
|
||||
}
|
||||
|
||||
export function createAnilistSetupDidFailLoadHandler(deps: {
|
||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => void;
|
||||
onLoadFailure: (details: {
|
||||
errorCode: number;
|
||||
errorDescription: string;
|
||||
validatedURL: string;
|
||||
}) => void;
|
||||
}) {
|
||||
return (details: { errorCode: number; errorDescription: string; validatedURL: string }): void => {
|
||||
deps.onLoadFailure(details);
|
||||
@@ -175,7 +179,11 @@ export function createAnilistSetupFallbackHandler(deps: {
|
||||
logWarn: (message: string) => void;
|
||||
}) {
|
||||
return {
|
||||
onLoadFailure: (details: { errorCode: number; errorDescription: string; validatedURL: string }) => {
|
||||
onLoadFailure: (details: {
|
||||
errorCode: number;
|
||||
errorDescription: string;
|
||||
validatedURL: string;
|
||||
}) => {
|
||||
deps.logError('AniList setup window failed to load', details);
|
||||
deps.openSetupInBrowser();
|
||||
if (!deps.setupWindow.isDestroyed()) {
|
||||
@@ -298,12 +306,7 @@ export function createOpenAnilistSetupWindowHandler<TWindow extends AnilistSetup
|
||||
});
|
||||
setupWindow.webContents.on(
|
||||
'did-fail-load',
|
||||
(
|
||||
_event: unknown,
|
||||
errorCode: number,
|
||||
errorDescription: string,
|
||||
validatedURL: string,
|
||||
) => {
|
||||
(_event: unknown, errorCode: number, errorDescription: string, validatedURL: string) => {
|
||||
handleDidFailLoad({
|
||||
errorCode,
|
||||
errorDescription,
|
||||
|
||||
@@ -65,9 +65,7 @@ export function findAnilistSetupDeepLinkArgvUrl(argv: readonly string[]): string
|
||||
return null;
|
||||
}
|
||||
|
||||
export function consumeAnilistSetupCallbackUrl(
|
||||
deps: ConsumeAnilistSetupCallbackUrlDeps,
|
||||
): boolean {
|
||||
export function consumeAnilistSetupCallbackUrl(deps: ConsumeAnilistSetupCallbackUrlDeps): boolean {
|
||||
const token = extractAnilistAccessTokenFromUrl(deps.rawUrl);
|
||||
if (!token) {
|
||||
return false;
|
||||
|
||||
@@ -12,7 +12,9 @@ type ConfigWithAnilistToken = {
|
||||
};
|
||||
};
|
||||
|
||||
export function createRefreshAnilistClientSecretStateHandler<TConfig extends ConfigWithAnilistToken>(deps: {
|
||||
export function createRefreshAnilistClientSecretStateHandler<
|
||||
TConfig extends ConfigWithAnilistToken,
|
||||
>(deps: {
|
||||
getResolvedConfig: () => TConfig;
|
||||
isAnilistTrackingEnabled: (config: TConfig) => boolean;
|
||||
getCachedAccessToken: () => string | null;
|
||||
|
||||
@@ -24,7 +24,9 @@ export function createBuildUpdateLastCardFromClipboardMainDepsHandler<TAnki>(dep
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildRefreshKnownWordCacheMainDepsHandler(deps: RefreshKnownWordCacheMainDeps) {
|
||||
export function createBuildRefreshKnownWordCacheMainDepsHandler(
|
||||
deps: RefreshKnownWordCacheMainDeps,
|
||||
) {
|
||||
return (): RefreshKnownWordCacheMainDeps => ({
|
||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||
missingIntegrationMessage: deps.missingIntegrationMessage,
|
||||
@@ -42,8 +44,10 @@ export function createBuildTriggerFieldGroupingMainDepsHandler<TAnki>(deps: {
|
||||
return () => ({
|
||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
triggerFieldGroupingCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
|
||||
deps.triggerFieldGroupingCore(options),
|
||||
triggerFieldGroupingCore: (options: {
|
||||
ankiIntegration: TAnki;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}) => deps.triggerFieldGroupingCore(options),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,8 +62,10 @@ export function createBuildMarkLastCardAsAudioCardMainDepsHandler<TAnki>(deps: {
|
||||
return () => ({
|
||||
getAnkiIntegration: () => deps.getAnkiIntegration(),
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
markLastCardAsAudioCardCore: (options: { ankiIntegration: TAnki; showMpvOsd: (text: string) => void }) =>
|
||||
deps.markLastCardAsAudioCardCore(options),
|
||||
markLastCardAsAudioCardCore: (options: {
|
||||
ankiIntegration: TAnki;
|
||||
showMpvOsd: (text: string) => void;
|
||||
}) => deps.markLastCardAsAudioCardCore(options),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
||||
destroyTray: () => calls.push('destroy-tray'),
|
||||
stopConfigHotReload: () => calls.push('stop-config'),
|
||||
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||
@@ -33,7 +34,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => {
|
||||
});
|
||||
|
||||
cleanup();
|
||||
assert.equal(calls.length, 21);
|
||||
assert.equal(calls.length, 22);
|
||||
assert.equal(calls[0], 'destroy-tray');
|
||||
assert.equal(calls[calls.length - 1], 'stop-discord-presence');
|
||||
assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
|
||||
@@ -58,11 +59,10 @@ test('restore windows on activate recreates windows then syncs visibility', () =
|
||||
const calls: string[] = [];
|
||||
const restore = createRestoreWindowsOnActivateHandler({
|
||||
createMainWindow: () => calls.push('main'),
|
||||
createInvisibleWindow: () => calls.push('invisible'),
|
||||
updateVisibleOverlayVisibility: () => calls.push('visible-sync'),
|
||||
updateInvisibleOverlayVisibility: () => calls.push('invisible-sync'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('mpv-sync'),
|
||||
});
|
||||
|
||||
restore();
|
||||
assert.deepEqual(calls, ['main', 'invisible', 'visible-sync', 'invisible-sync']);
|
||||
assert.deepEqual(calls, ['main', 'visible-sync', 'mpv-sync']);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
||||
destroyTray: () => void;
|
||||
stopConfigHotReload: () => void;
|
||||
restorePreviousSecondarySubVisibility: () => void;
|
||||
restoreMpvSubVisibility: () => void;
|
||||
unregisterAllGlobalShortcuts: () => void;
|
||||
stopSubtitleWebsocket: () => void;
|
||||
stopTexthookerService: () => void;
|
||||
@@ -25,6 +26,7 @@ export function createOnWillQuitCleanupHandler(deps: {
|
||||
deps.destroyTray();
|
||||
deps.stopConfigHotReload();
|
||||
deps.restorePreviousSecondarySubVisibility();
|
||||
deps.restoreMpvSubVisibility();
|
||||
deps.unregisterAllGlobalShortcuts();
|
||||
deps.stopSubtitleWebsocket();
|
||||
deps.stopTexthookerService();
|
||||
@@ -55,14 +57,12 @@ export function createShouldRestoreWindowsOnActivateHandler(deps: {
|
||||
|
||||
export function createRestoreWindowsOnActivateHandler(deps: {
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
}) {
|
||||
return (): void => {
|
||||
deps.createMainWindow();
|
||||
deps.createInvisibleWindow();
|
||||
deps.updateVisibleOverlayVisibility();
|
||||
deps.updateInvisibleOverlayVisibility();
|
||||
deps.syncOverlayMpvSubtitleSuppression();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ test('restore windows on activate deps builder maps all restoration callbacks',
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildRestoreWindowsOnActivateMainDepsHandler({
|
||||
createMainWindow: () => calls.push('main'),
|
||||
createInvisibleWindow: () => calls.push('invisible'),
|
||||
updateVisibleOverlayVisibility: () => calls.push('visible'),
|
||||
updateInvisibleOverlayVisibility: () => calls.push('invisible-visible'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('mpv-sync'),
|
||||
})();
|
||||
|
||||
deps.createMainWindow();
|
||||
deps.createInvisibleWindow();
|
||||
deps.updateVisibleOverlayVisibility();
|
||||
deps.updateInvisibleOverlayVisibility();
|
||||
assert.deepEqual(calls, ['main', 'invisible', 'visible', 'invisible-visible']);
|
||||
deps.syncOverlayMpvSubtitleSuppression();
|
||||
assert.deepEqual(calls, ['main', 'visible', 'mpv-sync']);
|
||||
});
|
||||
|
||||
@@ -10,14 +10,12 @@ export function createBuildShouldRestoreWindowsOnActivateMainDepsHandler(deps: {
|
||||
|
||||
export function createBuildRestoreWindowsOnActivateMainDepsHandler(deps: {
|
||||
createMainWindow: () => void;
|
||||
createInvisibleWindow: () => void;
|
||||
updateVisibleOverlayVisibility: () => void;
|
||||
updateInvisibleOverlayVisibility: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
createMainWindow: () => deps.createMainWindow(),
|
||||
createInvisibleWindow: () => deps.createInvisibleWindow(),
|
||||
updateVisibleOverlayVisibility: () => deps.updateVisibleOverlayVisibility(),
|
||||
updateInvisibleOverlayVisibility: () => deps.updateInvisibleOverlayVisibility(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects'
|
||||
destroyTray: () => calls.push('destroy-tray'),
|
||||
stopConfigHotReload: () => calls.push('stop-config'),
|
||||
restorePreviousSecondarySubVisibility: () => calls.push('restore-sub'),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
unregisterAllGlobalShortcuts: () => calls.push('unregister-shortcuts'),
|
||||
stopSubtitleWebsocket: () => calls.push('stop-ws'),
|
||||
stopTexthookerService: () => calls.push('stop-texthooker'),
|
||||
@@ -72,6 +73,7 @@ test('cleanup deps builder skips destroyed yomitan window', () => {
|
||||
destroyTray: () => {},
|
||||
stopConfigHotReload: () => {},
|
||||
restorePreviousSecondarySubVisibility: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
stopSubtitleWebsocket: () => {},
|
||||
stopTexthookerService: () => {},
|
||||
|
||||
@@ -21,6 +21,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
||||
destroyTray: () => void;
|
||||
stopConfigHotReload: () => void;
|
||||
restorePreviousSecondarySubVisibility: () => void;
|
||||
restoreMpvSubVisibility: () => void;
|
||||
unregisterAllGlobalShortcuts: () => void;
|
||||
stopSubtitleWebsocket: () => void;
|
||||
stopTexthookerService: () => void;
|
||||
@@ -51,6 +52,7 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: {
|
||||
destroyTray: () => deps.destroyTray(),
|
||||
stopConfigHotReload: () => deps.stopConfigHotReload(),
|
||||
restorePreviousSecondarySubVisibility: () => deps.restorePreviousSecondarySubVisibility(),
|
||||
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||
unregisterAllGlobalShortcuts: () => deps.unregisterAllGlobalShortcuts(),
|
||||
stopSubtitleWebsocket: () => deps.stopSubtitleWebsocket(),
|
||||
stopTexthookerService: () => deps.stopTexthookerService(),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { AppReadyRuntimeDepsFactoryInput } from '../app-lifecycle';
|
||||
|
||||
export function createBuildAppReadyRuntimeMainDepsHandler(
|
||||
deps: AppReadyRuntimeDepsFactoryInput,
|
||||
) {
|
||||
export function createBuildAppReadyRuntimeMainDepsHandler(deps: AppReadyRuntimeDepsFactoryInput) {
|
||||
return (): AppReadyRuntimeDepsFactoryInput => ({
|
||||
loadSubtitlePosition: deps.loadSubtitlePosition,
|
||||
resolveKeybindings: deps.resolveKeybindings,
|
||||
@@ -27,12 +25,12 @@ export function createBuildAppReadyRuntimeMainDepsHandler(
|
||||
prewarmSubtitleDictionaries: deps.prewarmSubtitleDictionaries,
|
||||
startBackgroundWarmups: deps.startBackgroundWarmups,
|
||||
texthookerOnlyMode: deps.texthookerOnlyMode,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig:
|
||||
deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: deps.shouldAutoInitializeOverlayRuntimeFromConfig,
|
||||
initializeOverlayRuntime: deps.initializeOverlayRuntime,
|
||||
handleInitialArgs: deps.handleInitialArgs,
|
||||
onCriticalConfigErrors: deps.onCriticalConfigErrors,
|
||||
logDebug: deps.logDebug,
|
||||
now: deps.now,
|
||||
shouldSkipHeavyStartup: deps.shouldSkipHeavyStartup,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,26 +50,18 @@ test('initialize overlay runtime main deps map build options and callbacks', ()
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
initializeOverlayRuntimeCore: (value) => {
|
||||
calls.push(`core:${JSON.stringify(value)}`);
|
||||
return { invisibleOverlayVisible: true };
|
||||
},
|
||||
buildOptions: () => options,
|
||||
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
|
||||
setOverlayRuntimeInitialized: (initialized) => calls.push(`set-initialized:${initialized}`),
|
||||
startBackgroundWarmups: () => calls.push('warmups'),
|
||||
})();
|
||||
|
||||
assert.equal(deps.isOverlayRuntimeInitialized(), false);
|
||||
assert.equal(deps.buildOptions(), options);
|
||||
assert.deepEqual(deps.initializeOverlayRuntimeCore(options), { invisibleOverlayVisible: true });
|
||||
deps.setInvisibleOverlayVisible(true);
|
||||
assert.equal(deps.initializeOverlayRuntimeCore(options), undefined);
|
||||
deps.setOverlayRuntimeInitialized(true);
|
||||
deps.startBackgroundWarmups();
|
||||
assert.deepEqual(calls, [
|
||||
'core:{"id":"opts"}',
|
||||
'set-invisible:true',
|
||||
'set-initialized:true',
|
||||
'warmups',
|
||||
]);
|
||||
assert.deepEqual(calls, ['core:{"id":"opts"}', 'set-initialized:true', 'warmups']);
|
||||
});
|
||||
|
||||
test('open yomitan settings main deps map async open callbacks', async () => {
|
||||
@@ -78,7 +70,8 @@ test('open yomitan settings main deps map async open callbacks', async () => {
|
||||
const extension = { id: 'ext' };
|
||||
const deps = createBuildOpenYomitanSettingsMainDepsHandler({
|
||||
ensureYomitanExtensionLoaded: async () => extension,
|
||||
openYomitanSettingsWindow: ({ yomitanExt }) => calls.push(`open:${(yomitanExt as { id: string }).id}`),
|
||||
openYomitanSettingsWindow: ({ yomitanExt }) =>
|
||||
calls.push(`open:${(yomitanExt as { id: string }).id}`),
|
||||
getExistingWindow: () => currentWindow,
|
||||
setWindow: (window) => {
|
||||
currentWindow = window;
|
||||
|
||||
@@ -45,9 +45,8 @@ export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
|
||||
|
||||
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean };
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => void;
|
||||
buildOptions: () => TOptions;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) => void;
|
||||
startBackgroundWarmups: () => void;
|
||||
}) {
|
||||
@@ -55,7 +54,6 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOpt
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
|
||||
buildOptions: () => deps.buildOptions(),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
setOverlayRuntimeInitialized: (initialized: boolean) =>
|
||||
deps.setOverlayRuntimeInitialized(initialized),
|
||||
startBackgroundWarmups: () => deps.startBackgroundWarmups(),
|
||||
@@ -68,6 +66,7 @@ export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWind
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
onWindowClosed?: () => void;
|
||||
}) => void;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
@@ -80,6 +79,7 @@ export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWind
|
||||
yomitanExt: TYomitanExt;
|
||||
getExistingWindow: () => TWindow | null;
|
||||
setWindow: (window: TWindow | null) => void;
|
||||
onWindowClosed?: () => void;
|
||||
}) => deps.openYomitanSettingsWindow(params),
|
||||
getExistingWindow: () => deps.getExistingWindow(),
|
||||
setWindow: (window: TWindow | null) => deps.setWindow(window),
|
||||
|
||||
@@ -18,9 +18,7 @@ test('build cli command context deps maps handlers and values', () => {
|
||||
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 () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CliArgs } from '../../cli/args';
|
||||
import type { CliCommandContextFactoryDeps } from './cli-command-context';
|
||||
|
||||
export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getMpvClient: CliCommandContextFactoryDeps['getMpvClient'];
|
||||
@@ -15,9 +16,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
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>;
|
||||
@@ -47,6 +46,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: deps.getSocketPath,
|
||||
setSocketPath: deps.setSocketPath,
|
||||
getMpvClient: deps.getMpvClient,
|
||||
@@ -60,9 +60,7 @@ export function createBuildCliCommandContextDepsHandler(deps: {
|
||||
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,
|
||||
|
||||
@@ -20,9 +20,7 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
showMpvOsd: (text) => calls.push(`osd:${text}`),
|
||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
|
||||
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
|
||||
copyCurrentSubtitle: () => calls.push('copy-sub'),
|
||||
startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`),
|
||||
mineSentenceCard: async () => {},
|
||||
@@ -73,16 +71,8 @@ test('cli command context factory composes main deps and context handlers', () =
|
||||
context.setSocketPath('/tmp/new.sock');
|
||||
context.showOsd('hello');
|
||||
context.setVisibleOverlay(true);
|
||||
context.setInvisibleOverlay(false);
|
||||
context.toggleVisibleOverlay();
|
||||
context.toggleInvisibleOverlay();
|
||||
|
||||
assert.equal(appState.mpvSocketPath, '/tmp/new.sock');
|
||||
assert.deepEqual(calls, [
|
||||
'osd:hello',
|
||||
'set-visible:true',
|
||||
'set-invisible:false',
|
||||
'toggle-visible',
|
||||
'toggle-invisible',
|
||||
]);
|
||||
assert.deepEqual(calls, ['osd:hello', 'set-visible:true', 'toggle-visible']);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,7 @@ import { createCliCommandContext } from './cli-command-context';
|
||||
import { createBuildCliCommandContextDepsHandler } from './cli-command-context-deps';
|
||||
import { createBuildCliCommandContextMainDepsHandler } from './cli-command-context-main-deps';
|
||||
|
||||
type CliCommandContextMainDeps = Parameters<
|
||||
typeof createBuildCliCommandContextMainDepsHandler
|
||||
>[0];
|
||||
type CliCommandContextMainDeps = Parameters<typeof createBuildCliCommandContextMainDepsHandler>[0];
|
||||
|
||||
export function createCliCommandContextFactory(deps: CliCommandContextMainDeps) {
|
||||
const buildCliCommandContextMainDepsHandler = createBuildCliCommandContextMainDepsHandler(deps);
|
||||
|
||||
@@ -23,9 +23,7 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
|
||||
initializeOverlayRuntime: () => calls.push('init-overlay'),
|
||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`set-visible:${visible}`),
|
||||
setInvisibleOverlayVisible: (visible) => calls.push(`set-invisible:${visible}`),
|
||||
|
||||
copyCurrentSubtitle: () => calls.push('copy-sub'),
|
||||
startPendingMultiCopy: (timeoutMs) => calls.push(`multi:${timeoutMs}`),
|
||||
@@ -103,16 +101,9 @@ test('cli command context main deps builder maps state and callbacks', async ()
|
||||
deps.showOsd('hello');
|
||||
deps.initializeOverlay();
|
||||
deps.setVisibleOverlay(true);
|
||||
deps.setInvisibleOverlay(false);
|
||||
deps.printHelp();
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
'osd:hello',
|
||||
'init-overlay',
|
||||
'set-visible:true',
|
||||
'set-invisible:false',
|
||||
'help',
|
||||
]);
|
||||
assert.deepEqual(calls, ['osd:hello', 'init-overlay', 'set-visible:true', 'help']);
|
||||
|
||||
const retry = await deps.retryAnilistQueueNow();
|
||||
assert.deepEqual(retry, { ok: true, message: 'ok' });
|
||||
|
||||
@@ -10,6 +10,7 @@ type CliCommandContextMainState = {
|
||||
|
||||
export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
appState: CliCommandContextMainState;
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
texthookerService: CliCommandContextFactoryDeps['texthookerService'];
|
||||
getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } };
|
||||
openExternal: (url: string) => Promise<unknown>;
|
||||
@@ -18,9 +19,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
|
||||
initializeOverlayRuntime: () => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
toggleInvisibleOverlay: () => void;
|
||||
setVisibleOverlayVisible: (visible: boolean) => void;
|
||||
setInvisibleOverlayVisible: (visible: boolean) => void;
|
||||
|
||||
copyCurrentSubtitle: () => void;
|
||||
startPendingMultiCopy: (timeoutMs: number) => void;
|
||||
@@ -53,6 +52,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
logError: (message: string, err: unknown) => void;
|
||||
}) {
|
||||
return (): CliCommandContextFactoryDeps => ({
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: () => deps.appState.mpvSocketPath,
|
||||
setSocketPath: (socketPath: string) => {
|
||||
deps.appState.mpvSocketPath = socketPath;
|
||||
@@ -70,9 +70,7 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
|
||||
isOverlayInitialized: () => deps.appState.overlayRuntimeInitialized,
|
||||
initializeOverlay: () => deps.initializeOverlayRuntime(),
|
||||
toggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
|
||||
toggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(),
|
||||
setVisibleOverlay: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlay: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
copyCurrentSubtitle: () => deps.copyCurrentSubtitle(),
|
||||
startPendingMultiCopy: (timeoutMs: number) => deps.startPendingMultiCopy(timeoutMs),
|
||||
mineSentenceCard: () => deps.mineSentenceCard(),
|
||||
|
||||
@@ -24,9 +24,7 @@ function createDeps() {
|
||||
isOverlayInitialized: () => true,
|
||||
initializeOverlay: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
toggleInvisibleOverlay: () => {},
|
||||
setVisibleOverlay: () => {},
|
||||
setInvisibleOverlay: () => {},
|
||||
copyCurrentSubtitle: () => {},
|
||||
startPendingMultiCopy: () => {},
|
||||
mineSentenceCard: async () => {},
|
||||
@@ -36,11 +34,11 @@ function createDeps() {
|
||||
triggerFieldGrouping: async () => {},
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
getAnilistStatus: () => ({} as never),
|
||||
getAnilistStatus: () => ({}) as never,
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetup: () => {},
|
||||
openJellyfinSetup: () => {},
|
||||
getAnilistQueueStatus: () => ({} as never),
|
||||
getAnilistQueueStatus: () => ({}) as never,
|
||||
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||
runJellyfinCommand: async () => {},
|
||||
openYomitanSettings: () => {},
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
type MpvClientLike = CliCommandRuntimeServiceContext['getClient'] extends () => infer T ? T : never;
|
||||
|
||||
export type CliCommandContextFactoryDeps = {
|
||||
setLogLevel?: (level: NonNullable<CliArgs['logLevel']>) => void;
|
||||
getSocketPath: () => string;
|
||||
setSocketPath: (socketPath: string) => void;
|
||||
getMpvClient: () => MpvClientLike;
|
||||
@@ -20,9 +21,7 @@ export type CliCommandContextFactoryDeps = {
|
||||
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>;
|
||||
@@ -56,6 +55,7 @@ export function createCliCommandContext(
|
||||
deps: CliCommandContextFactoryDeps,
|
||||
): CliCommandRuntimeServiceContext & CliCommandRuntimeServiceContextHandlers {
|
||||
return {
|
||||
setLogLevel: deps.setLogLevel,
|
||||
getSocketPath: deps.getSocketPath,
|
||||
setSocketPath: deps.setSocketPath,
|
||||
getClient: deps.getMpvClient,
|
||||
@@ -72,9 +72,7 @@ export function createCliCommandContext(
|
||||
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,
|
||||
|
||||
@@ -15,12 +15,11 @@ export function createCliCommandRuntimeHandler<TCliContext>(deps: {
|
||||
cliContext: TCliContext,
|
||||
) => void;
|
||||
}) {
|
||||
const handleTexthookerOnlyModeTransitionHandler =
|
||||
createHandleTexthookerOnlyModeTransitionHandler(
|
||||
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(
|
||||
deps.handleTexthookerOnlyModeTransitionMainDeps,
|
||||
)(),
|
||||
);
|
||||
const handleTexthookerOnlyModeTransitionHandler = createHandleTexthookerOnlyModeTransitionHandler(
|
||||
createBuildHandleTexthookerOnlyModeTransitionMainDepsHandler(
|
||||
deps.handleTexthookerOnlyModeTransitionMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
return (args: CliArgs, source: CliCommandSource = 'initial'): void => {
|
||||
handleTexthookerOnlyModeTransitionHandler(args);
|
||||
|
||||
@@ -13,9 +13,10 @@ export type AppendClipboardVideoToQueueRuntimeDeps = {
|
||||
sendMpvCommand: (command: (string | number)[]) => void;
|
||||
};
|
||||
|
||||
export function appendClipboardVideoToQueueRuntime(
|
||||
deps: AppendClipboardVideoToQueueRuntimeDeps,
|
||||
): { ok: boolean; message: string } {
|
||||
export function appendClipboardVideoToQueueRuntime(deps: AppendClipboardVideoToQueueRuntimeDeps): {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
} {
|
||||
const mpvClient = deps.getMpvClient();
|
||||
if (!mpvClient || !mpvClient.connected) {
|
||||
return { ok: false, message: 'MPV is not connected.' };
|
||||
|
||||
@@ -42,11 +42,13 @@ export function composeAppReadyRuntime(options: AppReadyComposerOptions): AppRea
|
||||
createBuildAppReadyRuntimeMainDepsHandler({
|
||||
...options.appReadyRuntimeMainDeps,
|
||||
reloadConfig,
|
||||
createImmersionTracker: createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(
|
||||
options.immersionTrackerStartupMainDeps,
|
||||
)(),
|
||||
),
|
||||
createImmersionTracker:
|
||||
options.appReadyRuntimeMainDeps.createImmersionTracker ??
|
||||
createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(
|
||||
options.immersionTrackerStartupMainDeps,
|
||||
)(),
|
||||
),
|
||||
onCriticalConfigErrors: criticalConfigError,
|
||||
})(),
|
||||
);
|
||||
|
||||
@@ -32,10 +32,8 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
||||
showMpvOsd: () => {},
|
||||
},
|
||||
mainDeps: {
|
||||
getInvisibleWindow: () => null,
|
||||
getMainWindow: () => null,
|
||||
getVisibleOverlayVisibility: () => false,
|
||||
getInvisibleOverlayVisibility: () => false,
|
||||
focusMainWindow: () => {},
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
@@ -44,7 +42,7 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
||||
tokenizeCurrentSubtitle: async () => null,
|
||||
getCurrentSubtitleRaw: () => '',
|
||||
getCurrentSubtitleAss: () => '',
|
||||
getMpvSubtitleRenderMetrics: () => ({}) as never,
|
||||
getPlaybackPaused: () => null,
|
||||
getSubtitlePosition: () => ({}) as never,
|
||||
getSubtitleStyle: () => ({}) as never,
|
||||
saveSubtitlePosition: () => {},
|
||||
@@ -56,7 +54,6 @@ test('composeIpcRuntimeHandlers returns callable IPC handlers and registration b
|
||||
getAnkiConnectStatus: () => false,
|
||||
getRuntimeOptions: () => [],
|
||||
reportOverlayContentBounds: () => {},
|
||||
reportHoveredSubtitleToken: () => {},
|
||||
getAnilistStatus: () => ({}) as never,
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetup: () => {},
|
||||
|
||||
@@ -6,7 +6,8 @@ test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers',
|
||||
let lastProgressAt = 0;
|
||||
const composed = composeJellyfinRemoteHandlers({
|
||||
getConfiguredSession: () => null,
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
|
||||
getClientInfo: () =>
|
||||
({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
|
||||
getJellyfinConfig: () => ({ enabled: false }) as never,
|
||||
playJellyfinItem: async () => {},
|
||||
logWarn: () => {},
|
||||
|
||||
@@ -26,6 +26,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
const calls: string[] = [];
|
||||
let started = false;
|
||||
let metrics = BASE_METRICS;
|
||||
let mecabTokenizer: { id: string } | null = null;
|
||||
|
||||
class FakeMpvClient {
|
||||
connected = false;
|
||||
@@ -68,12 +69,15 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
scheduleQuitCheck: () => {},
|
||||
quitApp: () => {},
|
||||
reportJellyfinRemoteStopped: () => {},
|
||||
syncOverlayMpvSubtitleSuppression: () => {},
|
||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||
logSubtitleTimingError: () => {},
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
ensureImmersionTrackerInitialized: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: () => {},
|
||||
maybeProbeAnilistDuration: () => {},
|
||||
@@ -90,7 +94,6 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
@@ -125,6 +128,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyDictionaryMatchMode: () => 'headword',
|
||||
getFrequencyRank: () => null,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => null,
|
||||
@@ -139,9 +143,15 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
return { text };
|
||||
},
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
setMecabTokenizer: () => {},
|
||||
createMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
getMecabTokenizer: () => mecabTokenizer,
|
||||
setMecabTokenizer: (next) => {
|
||||
mecabTokenizer = next as { id: string };
|
||||
calls.push('set-mecab');
|
||||
},
|
||||
createMecabTokenizer: () => {
|
||||
calls.push('create-mecab');
|
||||
return { id: 'mecab' };
|
||||
},
|
||||
checkAvailability: async () => {
|
||||
calls.push('check-mecab');
|
||||
},
|
||||
@@ -175,6 +185,10 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
calls.push('warmup-yomitan');
|
||||
},
|
||||
shouldWarmupMecab: () => true,
|
||||
shouldWarmupYomitanExtension: () => true,
|
||||
shouldWarmupSubtitleDictionaries: () => true,
|
||||
shouldWarmupJellyfinRemoteSession: () => true,
|
||||
shouldAutoConnectJellyfinRemote: () => false,
|
||||
startJellyfinRemoteSession: async () => {
|
||||
calls.push('warmup-jellyfin');
|
||||
@@ -189,6 +203,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
assert.equal(typeof composed.tokenizeSubtitle, 'function');
|
||||
assert.equal(typeof composed.createMecabTokenizerAndCheck, 'function');
|
||||
assert.equal(typeof composed.prewarmSubtitleDictionaries, 'function');
|
||||
assert.equal(typeof composed.startTokenizationWarmups, 'function');
|
||||
assert.equal(typeof composed.launchBackgroundWarmupTask, 'function');
|
||||
assert.equal(typeof composed.startBackgroundWarmups, 'function');
|
||||
|
||||
@@ -196,6 +211,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
assert.equal(client.connected, true);
|
||||
|
||||
composed.updateMpvSubtitleRenderMetrics({ subPos: 90 });
|
||||
await composed.startTokenizationWarmups();
|
||||
const tokenized = await composed.tokenizeSubtitle('subtitle text');
|
||||
await composed.createMecabTokenizerAndCheck();
|
||||
await composed.prewarmSubtitleDictionaries();
|
||||
@@ -211,9 +227,12 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
|
||||
assert.ok(calls.includes('broadcast-metrics'));
|
||||
assert.ok(calls.includes('create-tokenizer-runtime-deps'));
|
||||
assert.ok(calls.includes('tokenize:subtitle text'));
|
||||
assert.ok(calls.includes('create-mecab'));
|
||||
assert.ok(calls.includes('set-mecab'));
|
||||
assert.ok(calls.includes('check-mecab'));
|
||||
assert.ok(calls.includes('prewarm-jlpt'));
|
||||
assert.ok(calls.includes('prewarm-frequency'));
|
||||
assert.ok(calls.includes('set-started:true'));
|
||||
assert.ok(calls.includes('warmup-yomitan'));
|
||||
assert.ok(calls.indexOf('create-mecab') < calls.indexOf('set-started:true'));
|
||||
});
|
||||
|
||||
@@ -87,6 +87,7 @@ export type MpvRuntimeComposerResult<
|
||||
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||
startTokenizationWarmups: () => Promise<void>;
|
||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
}>;
|
||||
@@ -132,8 +133,23 @@ export function composeMpvRuntimeHandlers<
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||
);
|
||||
let tokenizationWarmupInFlight: Promise<void> | null = null;
|
||||
const startTokenizationWarmups = (): Promise<void> => {
|
||||
if (!tokenizationWarmupInFlight) {
|
||||
tokenizationWarmupInFlight = (async () => {
|
||||
await options.warmups.startBackgroundWarmupsMainDeps.ensureYomitanExtensionLoaded();
|
||||
if (!options.tokenizer.createMecabTokenizerAndCheckMainDeps.getMecabTokenizer()) {
|
||||
await createMecabTokenizerAndCheck().catch(() => {});
|
||||
}
|
||||
await prewarmSubtitleDictionaries({ showLoadingOsd: true });
|
||||
})().finally(() => {
|
||||
tokenizationWarmupInFlight = null;
|
||||
});
|
||||
}
|
||||
return tokenizationWarmupInFlight;
|
||||
};
|
||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||
await prewarmSubtitleDictionaries();
|
||||
await startTokenizationWarmups();
|
||||
return options.tokenizer.tokenizeSubtitle(
|
||||
text,
|
||||
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
||||
@@ -161,6 +177,7 @@ export function composeMpvRuntimeHandlers<
|
||||
tokenizeSubtitle,
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
startTokenizationWarmups,
|
||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ test('composeShortcutRuntimes returns callable shortcut runtime handlers', () =>
|
||||
getConfiguredShortcuts: () => ({}) as never,
|
||||
registerGlobalShortcutsCore: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
toggleInvisibleOverlay: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
isDev: false,
|
||||
getMainWindow: () => null,
|
||||
|
||||
@@ -16,6 +16,7 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
||||
destroyTray: () => {},
|
||||
stopConfigHotReload: () => {},
|
||||
restorePreviousSecondarySubVisibility: () => {},
|
||||
restoreMpvSubVisibility: () => {},
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
stopSubtitleWebsocket: () => {},
|
||||
stopTexthookerService: () => {},
|
||||
@@ -43,9 +44,8 @@ test('composeStartupLifecycleHandlers returns callable startup lifecycle handler
|
||||
},
|
||||
restoreWindowsOnActivateMainDeps: {
|
||||
createMainWindow: () => {},
|
||||
createInvisibleWindow: () => {},
|
||||
updateVisibleOverlayVisibility: () => {},
|
||||
updateInvisibleOverlayVisibility: () => {},
|
||||
syncOverlayMpvSubtitleSuppression: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import type { RuntimeOptionsManager } from '../../runtime-options';
|
||||
import type { JimakuApiResponse, JimakuLanguagePreference, ResolvedConfig } from '../../types';
|
||||
import {
|
||||
getInitialInvisibleOverlayVisibility as getInitialInvisibleOverlayVisibilityCore,
|
||||
getJimakuLanguagePreference as getJimakuLanguagePreferenceCore,
|
||||
getJimakuMaxEntryResults as getJimakuMaxEntryResultsCore,
|
||||
isAutoUpdateEnabledRuntime as isAutoUpdateEnabledRuntimeCore,
|
||||
jimakuFetchJson as jimakuFetchJsonCore,
|
||||
resolveJimakuApiKey as resolveJimakuApiKeyCore,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig as shouldAutoInitializeOverlayRuntimeFromConfigCore,
|
||||
shouldBindVisibleOverlayToMpvSubVisibility as shouldBindVisibleOverlayToMpvSubVisibilityCore,
|
||||
} from '../../core/services';
|
||||
|
||||
export type ConfigDerivedRuntimeDeps = {
|
||||
getResolvedConfig: () => ResolvedConfig;
|
||||
getRuntimeOptionsManager: () => RuntimeOptionsManager | null;
|
||||
platform: NodeJS.Platform;
|
||||
defaultJimakuLanguagePreference: JimakuLanguagePreference;
|
||||
defaultJimakuMaxEntryResults: number;
|
||||
defaultJimakuApiBaseUrl: string;
|
||||
};
|
||||
|
||||
export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
|
||||
getInitialInvisibleOverlayVisibility: () => boolean;
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => boolean;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isAutoUpdateEnabledRuntime: () => boolean;
|
||||
getJimakuLanguagePreference: () => JimakuLanguagePreference;
|
||||
getJimakuMaxEntryResults: () => number;
|
||||
@@ -34,12 +29,8 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
|
||||
) => Promise<JimakuApiResponse<T>>;
|
||||
} {
|
||||
return {
|
||||
getInitialInvisibleOverlayVisibility: () =>
|
||||
getInitialInvisibleOverlayVisibilityCore(deps.getResolvedConfig(), deps.platform),
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () =>
|
||||
shouldAutoInitializeOverlayRuntimeFromConfigCore(deps.getResolvedConfig()),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
shouldBindVisibleOverlayToMpvSubVisibilityCore(deps.getResolvedConfig()),
|
||||
isAutoUpdateEnabledRuntime: () =>
|
||||
isAutoUpdateEnabledRuntimeCore(deps.getResolvedConfig(), deps.getRuntimeOptionsManager()),
|
||||
getJimakuLanguagePreference: () =>
|
||||
@@ -48,7 +39,10 @@ export function createConfigDerivedRuntime(deps: ConfigDerivedRuntimeDeps): {
|
||||
deps.defaultJimakuLanguagePreference,
|
||||
),
|
||||
getJimakuMaxEntryResults: () =>
|
||||
getJimakuMaxEntryResultsCore(() => deps.getResolvedConfig(), deps.defaultJimakuMaxEntryResults),
|
||||
getJimakuMaxEntryResultsCore(
|
||||
() => deps.getResolvedConfig(),
|
||||
deps.defaultJimakuMaxEntryResults,
|
||||
),
|
||||
resolveJimakuApiKey: () => resolveJimakuApiKeyCore(() => deps.getResolvedConfig()),
|
||||
jimakuFetchJson: <T>(
|
||||
endpoint: string,
|
||||
|
||||
@@ -111,7 +111,7 @@ test('config hot reload applied main deps builder maps callbacks', () => {
|
||||
test('config hot reload runtime main deps builder maps runtime callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildConfigHotReloadRuntimeMainDepsHandler({
|
||||
getCurrentConfig: () => ({ id: 1 } as never as ResolvedConfig),
|
||||
getCurrentConfig: () => ({ id: 1 }) as never as ResolvedConfig,
|
||||
reloadConfigStrict: () =>
|
||||
({
|
||||
ok: true,
|
||||
@@ -144,5 +144,10 @@ test('config hot reload runtime main deps builder maps runtime callbacks', () =>
|
||||
deps.onRestartRequired([]);
|
||||
deps.onInvalidConfig('bad');
|
||||
deps.onValidationWarnings('/tmp/config.jsonc', []);
|
||||
assert.deepEqual(calls, ['hot-reload', 'restart-required', 'invalid-config', 'validation-warnings']);
|
||||
assert.deepEqual(calls, [
|
||||
'hot-reload',
|
||||
'restart-required',
|
||||
'invalid-config',
|
||||
'validation-warnings',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,12 @@ import type {
|
||||
ConfigHotReloadRuntimeDeps,
|
||||
} from '../../core/services/config-hot-reload';
|
||||
import type { ReloadConfigStrictResult } from '../../config';
|
||||
import type { ConfigHotReloadPayload, ConfigValidationWarning, ResolvedConfig, SecondarySubMode } from '../../types';
|
||||
import type {
|
||||
ConfigHotReloadPayload,
|
||||
ConfigValidationWarning,
|
||||
ResolvedConfig,
|
||||
SecondarySubMode,
|
||||
} from '../../types';
|
||||
import type { createConfigHotReloadMessageHandler } from './config-hot-reload-handlers';
|
||||
|
||||
type ConfigWatchListener = (eventType: string, filename: string | null) => void;
|
||||
|
||||
@@ -29,7 +29,8 @@ test('jlpt dictionary runtime main deps builder maps search paths and log prefix
|
||||
const deps = createBuildJlptDictionaryRuntimeMainDepsHandler({
|
||||
isJlptEnabled: () => true,
|
||||
getDictionaryRoots: () => ['/root/a'],
|
||||
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) => getDictionaryRoots().map((path) => `${path}/jlpt`),
|
||||
getJlptDictionarySearchPaths: ({ getDictionaryRoots }) =>
|
||||
getDictionaryRoots().map((path) => `${path}/jlpt`),
|
||||
setJlptLevelLookup: () => calls.push('set-lookup'),
|
||||
logInfo: (message) => calls.push(`log:${message}`),
|
||||
})();
|
||||
@@ -53,9 +54,9 @@ test('frequency dictionary roots main handler returns expected root list', () =>
|
||||
joinPath: (...parts) => parts.join('/'),
|
||||
})();
|
||||
|
||||
assert.equal(roots.length, 15);
|
||||
assert.equal(roots[0], '/repo/dist/main/../../vendor/jiten_freq_global');
|
||||
assert.equal(roots[14], '/repo');
|
||||
assert.equal(roots.length, 11);
|
||||
assert.equal(roots[0], '/repo/dist/main/../../vendor/frequency-dictionary');
|
||||
assert.equal(roots[10], '/repo');
|
||||
});
|
||||
|
||||
test('frequency dictionary runtime main deps builder maps search paths/source and log prefix', () => {
|
||||
|
||||
@@ -38,13 +38,9 @@ export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
|
||||
joinPath: (...parts: string[]) => string;
|
||||
}) {
|
||||
return () => [
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'jiten_freq_global'),
|
||||
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'frequency-dictionary'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'jiten_freq_global'),
|
||||
deps.joinPath(deps.appPath, 'vendor', 'frequency-dictionary'),
|
||||
deps.joinPath(deps.resourcesPath, 'jiten_freq_global'),
|
||||
deps.joinPath(deps.resourcesPath, 'frequency-dictionary'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'jiten_freq_global'),
|
||||
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'frequency-dictionary'),
|
||||
deps.userDataPath,
|
||||
deps.appUserDataPath,
|
||||
|
||||
@@ -15,9 +15,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
},
|
||||
}),
|
||||
getVisibleOverlayVisible: () => true,
|
||||
getInvisibleOverlayVisible: () => false,
|
||||
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
|
||||
setInvisibleOverlayVisible: (visible) => calls.push(`invisible:${visible}`),
|
||||
getResolver: () => resolver,
|
||||
setResolver: (nextResolver) => {
|
||||
calls.push(`set-resolver:${nextResolver ? 'set' : 'null'}`);
|
||||
@@ -31,17 +29,10 @@ test('field grouping overlay main deps builder maps window visibility and resolv
|
||||
|
||||
assert.equal(deps.getMainWindow()?.isDestroyed(), false);
|
||||
assert.equal(deps.getVisibleOverlayVisible(), true);
|
||||
assert.equal(deps.getInvisibleOverlayVisible(), false);
|
||||
assert.equal(deps.getResolver(), resolver);
|
||||
assert.equal(deps.getRestoreVisibleOverlayOnModalClose(), modalSet);
|
||||
deps.setVisibleOverlayVisible(true);
|
||||
deps.setInvisibleOverlayVisible(false);
|
||||
deps.setResolver(null);
|
||||
assert.equal(deps.sendToVisibleOverlay('kiku:open', 1), true);
|
||||
assert.deepEqual(calls, [
|
||||
'visible:true',
|
||||
'invisible:false',
|
||||
'set-resolver:null',
|
||||
'send:kiku:open:1',
|
||||
]);
|
||||
assert.deepEqual(calls, ['visible:true', 'set-resolver:null', 'send:kiku:open:1']);
|
||||
});
|
||||
|
||||
@@ -24,9 +24,7 @@ export function createBuildFieldGroupingOverlayMainDepsHandler<TModal extends st
|
||||
return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
|
||||
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
|
||||
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
|
||||
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
|
||||
getResolver: () => deps.getResolver(),
|
||||
setResolver: (resolver) => deps.setResolver(resolver),
|
||||
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),
|
||||
|
||||
@@ -12,23 +12,26 @@ test('get configured shortcuts main deps map config resolver inputs', () => {
|
||||
const build = createBuildGetConfiguredShortcutsMainDepsHandler({
|
||||
getResolvedConfig: () => config,
|
||||
defaultConfig: defaults,
|
||||
resolveConfiguredShortcuts: (nextConfig, nextDefaults) => ({ nextConfig, nextDefaults }) as never,
|
||||
resolveConfiguredShortcuts: (nextConfig, nextDefaults) =>
|
||||
({ nextConfig, nextDefaults }) as never,
|
||||
});
|
||||
|
||||
const deps = build();
|
||||
assert.equal(deps.getResolvedConfig(), config);
|
||||
assert.equal(deps.defaultConfig, defaults);
|
||||
assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), { nextConfig: config, nextDefaults: defaults });
|
||||
assert.deepEqual(deps.resolveConfiguredShortcuts(config, defaults), {
|
||||
nextConfig: config,
|
||||
nextDefaults: defaults,
|
||||
});
|
||||
});
|
||||
|
||||
test('register global shortcuts main deps map callbacks and flags', () => {
|
||||
const calls: string[] = [];
|
||||
const mainWindow = { id: 'main' };
|
||||
const build = createBuildRegisterGlobalShortcutsMainDepsHandler({
|
||||
getConfiguredShortcuts: () => ({ copySubtitle: 's' } as never),
|
||||
getConfiguredShortcuts: () => ({ copySubtitle: 's' }) as never,
|
||||
registerGlobalShortcutsCore: () => calls.push('register'),
|
||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
openYomitanSettings: () => calls.push('open-yomitan'),
|
||||
isDev: true,
|
||||
getMainWindow: () => mainWindow as never,
|
||||
@@ -38,17 +41,15 @@ test('register global shortcuts main deps map callbacks and flags', () => {
|
||||
deps.registerGlobalShortcutsCore({
|
||||
shortcuts: deps.getConfiguredShortcuts(),
|
||||
onToggleVisibleOverlay: () => undefined,
|
||||
onToggleInvisibleOverlay: () => undefined,
|
||||
onOpenYomitanSettings: () => undefined,
|
||||
isDev: deps.isDev,
|
||||
getMainWindow: deps.getMainWindow,
|
||||
});
|
||||
deps.onToggleVisibleOverlay();
|
||||
deps.onToggleInvisibleOverlay();
|
||||
deps.onOpenYomitanSettings();
|
||||
assert.equal(deps.isDev, true);
|
||||
assert.deepEqual(deps.getMainWindow(), mainWindow);
|
||||
assert.deepEqual(calls, ['register', 'toggle-visible', 'toggle-invisible', 'open-yomitan']);
|
||||
assert.deepEqual(calls, ['register', 'toggle-visible', 'open-yomitan']);
|
||||
});
|
||||
|
||||
test('refresh global shortcuts main deps map passthrough handlers', () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: {
|
||||
getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts'];
|
||||
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void;
|
||||
toggleVisibleOverlay: () => void;
|
||||
toggleInvisibleOverlay: () => void;
|
||||
openYomitanSettings: () => void;
|
||||
isDev: boolean;
|
||||
getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow'];
|
||||
@@ -29,7 +28,6 @@ export function createBuildRegisterGlobalShortcutsMainDepsHandler(deps: {
|
||||
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) =>
|
||||
deps.registerGlobalShortcutsCore(options),
|
||||
onToggleVisibleOverlay: () => deps.toggleVisibleOverlay(),
|
||||
onToggleInvisibleOverlay: () => deps.toggleInvisibleOverlay(),
|
||||
onOpenYomitanSettings: () => deps.openYomitanSettings(),
|
||||
isDev: deps.isDev,
|
||||
getMainWindow: deps.getMainWindow,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createGlobalShortcutsRuntimeHandlers } from './global-shortcuts-runtime
|
||||
function createShortcuts(): ConfiguredShortcuts {
|
||||
return {
|
||||
toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O',
|
||||
toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I',
|
||||
copySubtitle: 's',
|
||||
copySubtitleMultiple: 'CommandOrControl+s',
|
||||
updateLastCardFromClipboard: 'c',
|
||||
@@ -38,7 +37,6 @@ test('global shortcuts runtime handlers compose get/register/refresh flow', () =
|
||||
assert.equal(options.shortcuts, shortcuts);
|
||||
},
|
||||
toggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
toggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
openYomitanSettings: () => calls.push('open-yomitan'),
|
||||
isDev: false,
|
||||
getMainWindow: () => null,
|
||||
|
||||
@@ -32,15 +32,17 @@ export function createGlobalShortcutsRuntimeHandlers(deps: {
|
||||
const getConfiguredShortcutsMainDeps = createBuildGetConfiguredShortcutsMainDepsHandler(
|
||||
deps.getConfiguredShortcutsMainDeps,
|
||||
)();
|
||||
const getConfiguredShortcutsHandler =
|
||||
createGetConfiguredShortcutsHandler(getConfiguredShortcutsMainDeps);
|
||||
const getConfiguredShortcutsHandler = createGetConfiguredShortcutsHandler(
|
||||
getConfiguredShortcutsMainDeps,
|
||||
);
|
||||
const getConfiguredShortcuts = () => getConfiguredShortcutsHandler();
|
||||
|
||||
const registerGlobalShortcutsMainDeps = createBuildRegisterGlobalShortcutsMainDepsHandler(
|
||||
deps.buildRegisterGlobalShortcutsMainDeps(getConfiguredShortcuts),
|
||||
)();
|
||||
const registerGlobalShortcutsHandler =
|
||||
createRegisterGlobalShortcutsHandler(registerGlobalShortcutsMainDeps);
|
||||
const registerGlobalShortcutsHandler = createRegisterGlobalShortcutsHandler(
|
||||
registerGlobalShortcutsMainDeps,
|
||||
);
|
||||
const registerGlobalShortcuts = () => registerGlobalShortcutsHandler();
|
||||
|
||||
const refreshGlobalAndOverlayShortcutsMainDeps =
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { ConfiguredShortcuts } from '../../core/utils/shortcut-config';
|
||||
function createShortcuts(): ConfiguredShortcuts {
|
||||
return {
|
||||
toggleVisibleOverlayGlobal: 'CommandOrControl+Shift+O',
|
||||
toggleInvisibleOverlayGlobal: 'CommandOrControl+Shift+I',
|
||||
copySubtitle: 's',
|
||||
copySubtitleMultiple: 'CommandOrControl+s',
|
||||
updateLastCardFromClipboard: 'c',
|
||||
@@ -58,18 +57,16 @@ test('register global shortcuts handler passes through callbacks and shortcuts',
|
||||
assert.equal(options.isDev, true);
|
||||
assert.equal(options.getMainWindow(), mainWindow);
|
||||
options.onToggleVisibleOverlay();
|
||||
options.onToggleInvisibleOverlay();
|
||||
options.onOpenYomitanSettings();
|
||||
},
|
||||
onToggleVisibleOverlay: () => calls.push('toggle-visible'),
|
||||
onToggleInvisibleOverlay: () => calls.push('toggle-invisible'),
|
||||
onOpenYomitanSettings: () => calls.push('open-yomitan'),
|
||||
isDev: true,
|
||||
getMainWindow: () => mainWindow,
|
||||
});
|
||||
|
||||
registerGlobalShortcuts();
|
||||
assert.deepEqual(calls, ['register', 'toggle-visible', 'toggle-invisible', 'open-yomitan']);
|
||||
assert.deepEqual(calls, ['register', 'toggle-visible', 'open-yomitan']);
|
||||
});
|
||||
|
||||
test('refresh global and overlay shortcuts unregisters then re-registers', () => {
|
||||
|
||||
@@ -5,10 +5,7 @@ import type { RegisterGlobalShortcutsServiceOptions } from '../../core/services/
|
||||
export function createGetConfiguredShortcutsHandler(deps: {
|
||||
getResolvedConfig: () => Config;
|
||||
defaultConfig: Config;
|
||||
resolveConfiguredShortcuts: (
|
||||
config: Config,
|
||||
defaultConfig: Config,
|
||||
) => ConfiguredShortcuts;
|
||||
resolveConfiguredShortcuts: (config: Config, defaultConfig: Config) => ConfiguredShortcuts;
|
||||
}) {
|
||||
return (): ConfiguredShortcuts =>
|
||||
deps.resolveConfiguredShortcuts(deps.getResolvedConfig(), deps.defaultConfig);
|
||||
@@ -18,7 +15,6 @@ export function createRegisterGlobalShortcutsHandler(deps: {
|
||||
getConfiguredShortcuts: () => RegisterGlobalShortcutsServiceOptions['shortcuts'];
|
||||
registerGlobalShortcutsCore: (options: RegisterGlobalShortcutsServiceOptions) => void;
|
||||
onToggleVisibleOverlay: () => void;
|
||||
onToggleInvisibleOverlay: () => void;
|
||||
onOpenYomitanSettings: () => void;
|
||||
isDev: boolean;
|
||||
getMainWindow: RegisterGlobalShortcutsServiceOptions['getMainWindow'];
|
||||
@@ -27,7 +23,6 @@ export function createRegisterGlobalShortcutsHandler(deps: {
|
||||
deps.registerGlobalShortcutsCore({
|
||||
shortcuts: deps.getConfiguredShortcuts(),
|
||||
onToggleVisibleOverlay: deps.onToggleVisibleOverlay,
|
||||
onToggleInvisibleOverlay: deps.onToggleInvisibleOverlay,
|
||||
onOpenYomitanSettings: deps.onOpenYomitanSettings,
|
||||
isDev: deps.isDev,
|
||||
getMainWindow: deps.getMainWindow,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createBuildImmersionTrackerStartupMainDepsHandler } from './immersion-s
|
||||
test('immersion tracker startup main deps builder maps callbacks', () => {
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildImmersionTrackerStartupMainDepsHandler({
|
||||
getResolvedConfig: () => ({ immersionTracking: { enabled: true } } as never),
|
||||
getResolvedConfig: () => ({ immersionTracking: { enabled: true } }) as never,
|
||||
getConfiguredDbPath: () => '/tmp/immersion.db',
|
||||
createTrackerService: () => {
|
||||
calls.push('create');
|
||||
@@ -21,9 +21,12 @@ test('immersion tracker startup main deps builder maps callbacks', () => {
|
||||
|
||||
assert.deepEqual(deps.getResolvedConfig(), { immersionTracking: { enabled: true } });
|
||||
assert.equal(deps.getConfiguredDbPath(), '/tmp/immersion.db');
|
||||
assert.deepEqual(deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }), {
|
||||
id: 'tracker',
|
||||
});
|
||||
assert.deepEqual(
|
||||
deps.createTrackerService({ dbPath: '/tmp/immersion.db', policy: {} as never }),
|
||||
{
|
||||
id: 'tracker',
|
||||
},
|
||||
);
|
||||
deps.setTracker(null);
|
||||
assert.equal(deps.getMpvClient()?.connected, true);
|
||||
deps.seedTrackerFromCurrentMedia();
|
||||
|
||||
@@ -24,7 +24,7 @@ test('initial args handler no-ops without initial args', () => {
|
||||
test('initial args handler ensures tray in background mode', () => {
|
||||
let ensuredTray = false;
|
||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||
getInitialArgs: () => ({ start: true } as never),
|
||||
getInitialArgs: () => ({ start: true }) as never,
|
||||
isBackgroundMode: () => true,
|
||||
ensureTray: () => {
|
||||
ensuredTray = true;
|
||||
@@ -44,7 +44,7 @@ test('initial args handler auto-connects mpv when needed', () => {
|
||||
let connectCalls = 0;
|
||||
let logged = false;
|
||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||
getInitialArgs: () => ({ start: true } as never),
|
||||
getInitialArgs: () => ({ start: true }) as never,
|
||||
isBackgroundMode: () => false,
|
||||
ensureTray: () => {},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
@@ -69,7 +69,7 @@ test('initial args handler auto-connects mpv when needed', () => {
|
||||
test('initial args handler forwards args to cli handler', () => {
|
||||
const seenSources: string[] = [];
|
||||
const handleInitialArgs = createHandleInitialArgsHandler({
|
||||
getInitialArgs: () => ({ start: true } as never),
|
||||
getInitialArgs: () => ({ start: true }) as never,
|
||||
isBackgroundMode: () => false,
|
||||
ensureTray: () => {},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createInitialArgsRuntimeHandler } from './initial-args-runtime-handler'
|
||||
test('initial args runtime handler composes main deps and runs initial command flow', () => {
|
||||
const calls: string[] = [];
|
||||
const handleInitialArgs = createInitialArgsRuntimeHandler({
|
||||
getInitialArgs: () => ({ start: true } as never),
|
||||
getInitialArgs: () => ({ start: true }) as never,
|
||||
isBackgroundMode: () => true,
|
||||
ensureTray: () => calls.push('tray'),
|
||||
isTexthookerOnlyMode: () => false,
|
||||
@@ -20,5 +20,10 @@ test('initial args runtime handler composes main deps and runs initial command f
|
||||
|
||||
handleInitialArgs();
|
||||
|
||||
assert.deepEqual(calls, ['tray', 'log:Auto-connecting MPV client for immersion tracking', 'connect', 'cli:initial']);
|
||||
assert.deepEqual(calls, [
|
||||
'tray',
|
||||
'log:Auto-connecting MPV client for immersion tracking',
|
||||
'connect',
|
||||
'cli:initial',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { MpvCommandFromIpcRuntimeDeps } from '../ipc-mpv-command';
|
||||
|
||||
export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(deps: MpvCommandFromIpcRuntimeDeps) {
|
||||
export function createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
||||
deps: MpvCommandFromIpcRuntimeDeps,
|
||||
) {
|
||||
return (): MpvCommandFromIpcRuntimeDeps => ({
|
||||
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
|
||||
openRuntimeOptionsPalette: () => deps.openRuntimeOptionsPalette(),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { createHandleMpvCommandFromIpcHandler, createRunSubsyncManualFromIpcHandler } from './ipc-bridge-actions';
|
||||
import {
|
||||
createHandleMpvCommandFromIpcHandler,
|
||||
createRunSubsyncManualFromIpcHandler,
|
||||
} from './ipc-bridge-actions';
|
||||
import {
|
||||
createBuildHandleMpvCommandFromIpcMainDepsHandler,
|
||||
createBuildRunSubsyncManualFromIpcMainDepsHandler,
|
||||
@@ -22,10 +25,10 @@ export function createIpcRuntimeHandlers<TRequest, TResult>(deps: {
|
||||
handleMpvCommandFromIpcMainDeps,
|
||||
);
|
||||
|
||||
const runSubsyncManualFromIpcMainDeps =
|
||||
createBuildRunSubsyncManualFromIpcMainDepsHandler<TRequest, TResult>(
|
||||
deps.runSubsyncManualFromIpcDeps,
|
||||
)();
|
||||
const runSubsyncManualFromIpcMainDeps = createBuildRunSubsyncManualFromIpcMainDepsHandler<
|
||||
TRequest,
|
||||
TResult
|
||||
>(deps.runSubsyncManualFromIpcDeps)();
|
||||
const runSubsyncManualFromIpc = createRunSubsyncManualFromIpcHandler<TRequest, TResult>(
|
||||
runSubsyncManualFromIpcMainDeps,
|
||||
);
|
||||
|
||||
@@ -94,7 +94,11 @@ export function createHandleJellyfinListCommands(deps: {
|
||||
if (!args.jellyfinItemId) {
|
||||
throw new Error('Missing --jellyfin-item-id for --jellyfin-subtitles.');
|
||||
}
|
||||
const tracks = await deps.listJellyfinSubtitleTracks(session, clientInfo, args.jellyfinItemId);
|
||||
const tracks = await deps.listJellyfinSubtitleTracks(
|
||||
session,
|
||||
clientInfo,
|
||||
args.jellyfinItemId,
|
||||
);
|
||||
if (tracks.length === 0) {
|
||||
deps.logInfo('No Jellyfin subtitle tracks found for item.');
|
||||
return true;
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import type {
|
||||
createHandleJellyfinAuthCommands,
|
||||
} from './jellyfin-cli-auth';
|
||||
import type {
|
||||
createHandleJellyfinListCommands,
|
||||
} from './jellyfin-cli-list';
|
||||
import type {
|
||||
createHandleJellyfinPlayCommand,
|
||||
} from './jellyfin-cli-play';
|
||||
import type {
|
||||
createHandleJellyfinRemoteAnnounceCommand,
|
||||
} from './jellyfin-cli-remote-announce';
|
||||
import type { createHandleJellyfinAuthCommands } from './jellyfin-cli-auth';
|
||||
import type { createHandleJellyfinListCommands } from './jellyfin-cli-list';
|
||||
import type { createHandleJellyfinPlayCommand } from './jellyfin-cli-play';
|
||||
import type { createHandleJellyfinRemoteAnnounceCommand } from './jellyfin-cli-remote-announce';
|
||||
|
||||
type HandleJellyfinAuthCommandsMainDeps = Parameters<typeof createHandleJellyfinAuthCommands>[0];
|
||||
type HandleJellyfinListCommandsMainDeps = Parameters<typeof createHandleJellyfinListCommands>[0];
|
||||
|
||||
@@ -3,7 +3,9 @@ import type {
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
} from './jellyfin-client-info';
|
||||
|
||||
type GetResolvedJellyfinConfigMainDeps = Parameters<typeof createGetResolvedJellyfinConfigHandler>[0];
|
||||
type GetResolvedJellyfinConfigMainDeps = Parameters<
|
||||
typeof createGetResolvedJellyfinConfigHandler
|
||||
>[0];
|
||||
type GetJellyfinClientInfoMainDeps = Parameters<typeof createGetJellyfinClientInfoHandler>[0];
|
||||
|
||||
export function createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
test('get resolved jellyfin config returns jellyfin section from resolved config', () => {
|
||||
const jellyfin = { url: 'https://jellyfin.local' } as never;
|
||||
const getConfig = createGetResolvedJellyfinConfigHandler({
|
||||
getResolvedConfig: () => ({ jellyfin } as never),
|
||||
getResolvedConfig: () => ({ jellyfin }) as never,
|
||||
loadStoredSession: () => null,
|
||||
getEnv: () => undefined,
|
||||
});
|
||||
@@ -68,8 +68,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
|
||||
},
|
||||
}) as never,
|
||||
loadStoredSession: () => ({ accessToken: 'stored-token', userId: 'stored-user' }),
|
||||
getEnv: (key: string) =>
|
||||
key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined,
|
||||
getEnv: (key: string) => (key === 'SUBMINER_JELLYFIN_ACCESS_TOKEN' ? 'env-token' : undefined),
|
||||
});
|
||||
|
||||
assert.deepEqual(getConfig(), {
|
||||
@@ -81,7 +80,7 @@ test('get resolved jellyfin config uses stored user id when env token set withou
|
||||
|
||||
test('jellyfin client info resolves defaults when fields are missing', () => {
|
||||
const getClientInfo = createGetJellyfinClientInfoHandler({
|
||||
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' } as never),
|
||||
getResolvedJellyfinConfig: () => ({ clientName: '', clientVersion: '', deviceId: '' }) as never,
|
||||
getDefaultJellyfinConfig: () =>
|
||||
({
|
||||
clientName: 'SubMiner',
|
||||
|
||||
@@ -24,9 +24,6 @@ function createArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
toggleOverlay: false,
|
||||
hideOverlay: false,
|
||||
showOverlay: false,
|
||||
toggleInvisibleOverlay: false,
|
||||
hideInvisibleOverlay: false,
|
||||
showInvisibleOverlay: false,
|
||||
copyCurrentSubtitle: false,
|
||||
multiCopy: false,
|
||||
mineSentence: false,
|
||||
|
||||
@@ -2,7 +2,9 @@ import type { createPlayJellyfinItemInMpvHandler } from './jellyfin-playback-lau
|
||||
|
||||
type PlayJellyfinItemInMpvMainDeps = Parameters<typeof createPlayJellyfinItemInMpvHandler>[0];
|
||||
|
||||
export function createBuildPlayJellyfinItemInMpvMainDepsHandler(deps: PlayJellyfinItemInMpvMainDeps) {
|
||||
export function createBuildPlayJellyfinItemInMpvMainDepsHandler(
|
||||
deps: PlayJellyfinItemInMpvMainDeps,
|
||||
) {
|
||||
return (): PlayJellyfinItemInMpvMainDeps => ({
|
||||
ensureMpvConnectedForPlayback: () => deps.ensureMpvConnectedForPlayback(),
|
||||
getMpvClient: () => deps.getMpvClient(),
|
||||
|
||||
@@ -136,6 +136,8 @@ test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices',
|
||||
assert.equal(playback.subtitleStreamIndex, null);
|
||||
assert.ok(calls.includes('progress:true'));
|
||||
assert.ok(
|
||||
calls.some((entry) => entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand')),
|
||||
calls.some((entry) =>
|
||||
entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,7 +44,9 @@ export type JellyfinRemoteProgressReporterDeps = {
|
||||
logDebug: (message: string, error: unknown) => void;
|
||||
};
|
||||
|
||||
export function createReportJellyfinRemoteProgressHandler(deps: JellyfinRemoteProgressReporterDeps) {
|
||||
export function createReportJellyfinRemoteProgressHandler(
|
||||
deps: JellyfinRemoteProgressReporterDeps,
|
||||
) {
|
||||
return async (force = false): Promise<void> => {
|
||||
const playback = deps.getActivePlayback();
|
||||
if (!playback) return;
|
||||
|
||||
@@ -3,8 +3,12 @@ import type {
|
||||
createStopJellyfinRemoteSessionHandler,
|
||||
} from './jellyfin-remote-session-lifecycle';
|
||||
|
||||
type StartJellyfinRemoteSessionMainDeps = Parameters<typeof createStartJellyfinRemoteSessionHandler>[0];
|
||||
type StopJellyfinRemoteSessionMainDeps = Parameters<typeof createStopJellyfinRemoteSessionHandler>[0];
|
||||
type StartJellyfinRemoteSessionMainDeps = Parameters<
|
||||
typeof createStartJellyfinRemoteSessionHandler
|
||||
>[0];
|
||||
type StopJellyfinRemoteSessionMainDeps = Parameters<
|
||||
typeof createStopJellyfinRemoteSessionHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildStartJellyfinRemoteSessionMainDepsHandler(
|
||||
deps: StartJellyfinRemoteSessionMainDeps,
|
||||
|
||||
@@ -16,7 +16,11 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
}),
|
||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'dev' }),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'dev',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: (message) => calls.push(`info:${message}`),
|
||||
@@ -38,12 +42,15 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
||||
username: 'u',
|
||||
password: 'p',
|
||||
});
|
||||
assert.deepEqual(await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()), {
|
||||
serverUrl: 'http://127.0.0.1:8096',
|
||||
username: 'alice',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
});
|
||||
assert.deepEqual(
|
||||
await deps.authenticateWithPassword('s', 'u', 'p', deps.getJellyfinClientInfo()),
|
||||
{
|
||||
serverUrl: 'http://127.0.0.1:8096',
|
||||
username: 'alice',
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
},
|
||||
);
|
||||
deps.saveStoredSession({ accessToken: 'token', userId: 'uid' });
|
||||
deps.patchJellyfinConfig({
|
||||
serverUrl: 'http://127.0.0.1:8096',
|
||||
@@ -57,5 +64,13 @@ test('open jellyfin setup window main deps builder maps callbacks', async () =>
|
||||
deps.clearSetupWindow();
|
||||
deps.setSetupWindow({} as never);
|
||||
assert.equal(deps.encodeURIComponent('a b'), 'a%20b');
|
||||
assert.deepEqual(calls, ['save', 'patch', 'info:ok', 'error:bad', 'osd:toast', 'clear', 'set-window']);
|
||||
assert.deepEqual(calls, [
|
||||
'save',
|
||||
'patch',
|
||||
'info:ok',
|
||||
'error:bad',
|
||||
'osd:toast',
|
||||
'clear',
|
||||
'set-window',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -50,7 +50,11 @@ test('createHandleJellyfinSetupSubmissionHandler applies successful login', asyn
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
}),
|
||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: (session) => {
|
||||
savedSession = session;
|
||||
calls.push('save');
|
||||
@@ -86,7 +90,11 @@ test('createHandleJellyfinSetupSubmissionHandler reports failure to OSD', async
|
||||
authenticateWithPassword: async () => {
|
||||
throw new Error('bad credentials');
|
||||
},
|
||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
@@ -180,7 +188,11 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
||||
authenticateWithPassword: async () => {
|
||||
throw new Error('should not auth');
|
||||
},
|
||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => {},
|
||||
patchJellyfinConfig: () => {},
|
||||
logInfo: () => {},
|
||||
@@ -196,14 +208,18 @@ test('createOpenJellyfinSetupWindowHandler no-ops when existing setup window is
|
||||
});
|
||||
|
||||
test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window lifecycle', async () => {
|
||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null = null;
|
||||
let willNavigateHandler: ((event: { preventDefault: () => void }, url: string) => void) | null =
|
||||
null;
|
||||
let closedHandler: (() => void) | null = null;
|
||||
let prevented = false;
|
||||
const calls: string[] = [];
|
||||
const fakeWindow = {
|
||||
focus: () => {},
|
||||
webContents: {
|
||||
on: (event: 'will-navigate', handler: (event: { preventDefault: () => void }, url: string) => void) => {
|
||||
on: (
|
||||
event: 'will-navigate',
|
||||
handler: (event: { preventDefault: () => void }, url: string) => void,
|
||||
) => {
|
||||
if (event === 'will-navigate') {
|
||||
willNavigateHandler = handler;
|
||||
}
|
||||
@@ -233,7 +249,11 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
accessToken: 'token',
|
||||
userId: 'uid',
|
||||
}),
|
||||
getJellyfinClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'did' }),
|
||||
getJellyfinClientInfo: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: '1.0',
|
||||
deviceId: 'did',
|
||||
}),
|
||||
saveStoredSession: () => calls.push('save'),
|
||||
patchJellyfinConfig: () => calls.push('patch'),
|
||||
logInfo: () => calls.push('info'),
|
||||
@@ -249,7 +269,9 @@ test('createOpenJellyfinSetupWindowHandler wires navigation, load, and window li
|
||||
assert.ok(closedHandler);
|
||||
assert.deepEqual(calls.slice(0, 2), ['load:data-url', 'set-window']);
|
||||
|
||||
const navHandler = willNavigateHandler as ((event: { preventDefault: () => void }, url: string) => void) | null;
|
||||
const navHandler = willNavigateHandler as
|
||||
| ((event: { preventDefault: () => void }, url: string) => void)
|
||||
| null;
|
||||
if (!navHandler) {
|
||||
throw new Error('missing will-navigate handler');
|
||||
}
|
||||
|
||||
@@ -109,7 +109,9 @@ export function parseJellyfinSetupSubmissionUrl(rawUrl: string): {
|
||||
}
|
||||
|
||||
export function createHandleJellyfinSetupSubmissionHandler(deps: {
|
||||
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null;
|
||||
parseSubmissionUrl: (
|
||||
rawUrl: string,
|
||||
) => { server: string; username: string; password: string } | null;
|
||||
authenticateWithPassword: (
|
||||
server: string,
|
||||
username: string,
|
||||
@@ -179,20 +181,22 @@ export function createHandleJellyfinSetupWindowClosedHandler(deps: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleJellyfinSetupWindowOpenedHandler(deps: {
|
||||
setSetupWindow: () => void;
|
||||
}) {
|
||||
export function createHandleJellyfinSetupWindowOpenedHandler(deps: { setSetupWindow: () => void }) {
|
||||
return (): void => {
|
||||
deps.setSetupWindow();
|
||||
};
|
||||
}
|
||||
|
||||
export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSetupWindowLike>(deps: {
|
||||
export function createOpenJellyfinSetupWindowHandler<
|
||||
TWindow extends JellyfinSetupWindowLike,
|
||||
>(deps: {
|
||||
maybeFocusExistingSetupWindow: () => boolean;
|
||||
createSetupWindow: () => TWindow;
|
||||
getResolvedJellyfinConfig: () => { serverUrl?: string | null; username?: string | null };
|
||||
buildSetupFormHtml: (defaultServer: string, defaultUser: string) => string;
|
||||
parseSubmissionUrl: (rawUrl: string) => { server: string; username: string; password: string } | null;
|
||||
parseSubmissionUrl: (
|
||||
rawUrl: string,
|
||||
) => { server: string; username: string; password: string } | null;
|
||||
authenticateWithPassword: (
|
||||
server: string,
|
||||
username: string,
|
||||
@@ -258,9 +262,7 @@ export function createOpenJellyfinSetupWindowHandler<TWindow extends JellyfinSet
|
||||
},
|
||||
});
|
||||
});
|
||||
void setupWindow.loadURL(
|
||||
`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`,
|
||||
);
|
||||
void setupWindow.loadURL(`data:text/html;charset=utf-8,${deps.encodeURIComponent(formHtml)}`);
|
||||
setupWindow.on('closed', () => {
|
||||
handleWindowClosed();
|
||||
});
|
||||
|
||||
@@ -117,13 +117,7 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
|
||||
seenUrls.add(track.deliveryUrl);
|
||||
const labelBase = (track.title || track.language || '').trim();
|
||||
const label = labelBase || `Jellyfin Subtitle ${track.index}`;
|
||||
deps.sendMpvCommand([
|
||||
'sub-add',
|
||||
track.deliveryUrl,
|
||||
'cached',
|
||||
label,
|
||||
track.language || '',
|
||||
]);
|
||||
deps.sendMpvCommand(['sub-add', track.deliveryUrl, 'cached', label, track.language || '']);
|
||||
}
|
||||
|
||||
await deps.wait(250);
|
||||
|
||||
@@ -39,27 +39,28 @@ export function createCopyCurrentSubtitleHandler<TSubtitleTimingTracker>(deps: {
|
||||
};
|
||||
}
|
||||
|
||||
export function createHandleMineSentenceDigitHandler<TSubtitleTimingTracker, TAnkiIntegration>(
|
||||
deps: {
|
||||
getSubtitleTimingTracker: () => TSubtitleTimingTracker;
|
||||
getAnkiIntegration: () => TAnkiIntegration;
|
||||
getCurrentSecondarySubText: () => string | undefined;
|
||||
showMpvOsd: (text: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
onCardsMined: (count: number) => void;
|
||||
handleMineSentenceDigitCore: (
|
||||
count: number,
|
||||
options: {
|
||||
subtitleTimingTracker: TSubtitleTimingTracker;
|
||||
ankiIntegration: TAnkiIntegration;
|
||||
getCurrentSecondarySubText: () => string | undefined;
|
||||
showMpvOsd: (text: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
onCardsMined: (count: number) => void;
|
||||
},
|
||||
) => void;
|
||||
},
|
||||
) {
|
||||
export function createHandleMineSentenceDigitHandler<
|
||||
TSubtitleTimingTracker,
|
||||
TAnkiIntegration,
|
||||
>(deps: {
|
||||
getSubtitleTimingTracker: () => TSubtitleTimingTracker;
|
||||
getAnkiIntegration: () => TAnkiIntegration;
|
||||
getCurrentSecondarySubText: () => string | undefined;
|
||||
showMpvOsd: (text: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
onCardsMined: (count: number) => void;
|
||||
handleMineSentenceDigitCore: (
|
||||
count: number,
|
||||
options: {
|
||||
subtitleTimingTracker: TSubtitleTimingTracker;
|
||||
ankiIntegration: TAnkiIntegration;
|
||||
getCurrentSecondarySubText: () => string | undefined;
|
||||
showMpvOsd: (text: string) => void;
|
||||
logError: (message: string, err: unknown) => void;
|
||||
onCardsMined: (count: number) => void;
|
||||
},
|
||||
) => void;
|
||||
}) {
|
||||
return (count: number): void => {
|
||||
deps.handleMineSentenceDigitCore(count, {
|
||||
subtitleTimingTracker: deps.getSubtitleTimingTracker(),
|
||||
|
||||
@@ -11,6 +11,7 @@ test('mpv connection handler reports stop and quits when disconnect guard passes
|
||||
const handler = createHandleMpvConnectionChangeHandler({
|
||||
reportJellyfinRemoteStopped: () => calls.push('report-stop'),
|
||||
refreshDiscordPresence: () => calls.push('presence-refresh'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||
hasInitialJellyfinPlayArg: () => true,
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
isQuitOnDisconnectArmed: () => true,
|
||||
@@ -26,6 +27,27 @@ test('mpv connection handler reports stop and quits when disconnect guard passes
|
||||
assert.deepEqual(calls, ['presence-refresh', 'report-stop', 'schedule', 'quit']);
|
||||
});
|
||||
|
||||
test('mpv connection handler syncs overlay subtitle suppression on connect', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createHandleMpvConnectionChangeHandler({
|
||||
reportJellyfinRemoteStopped: () => calls.push('report-stop'),
|
||||
refreshDiscordPresence: () => calls.push('presence-refresh'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||
hasInitialJellyfinPlayArg: () => true,
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
isQuitOnDisconnectArmed: () => true,
|
||||
scheduleQuitCheck: () => {
|
||||
calls.push('schedule');
|
||||
},
|
||||
isMpvConnected: () => false,
|
||||
quitApp: () => calls.push('quit'),
|
||||
});
|
||||
|
||||
handler({ connected: true });
|
||||
|
||||
assert.deepEqual(calls, ['presence-refresh', 'sync-overlay-mpv-sub']);
|
||||
});
|
||||
|
||||
test('mpv subtitle timing handler ignores blank subtitle lines', () => {
|
||||
const calls: string[] = [];
|
||||
const handler = createHandleMpvSubtitleTimingHandler({
|
||||
|
||||
@@ -18,6 +18,7 @@ type MpvEventClient = {
|
||||
export function createHandleMpvConnectionChangeHandler(deps: {
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
refreshDiscordPresence: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
hasInitialJellyfinPlayArg: () => boolean;
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
isQuitOnDisconnectArmed: () => boolean;
|
||||
@@ -27,7 +28,10 @@ export function createHandleMpvConnectionChangeHandler(deps: {
|
||||
}) {
|
||||
return ({ connected }: { connected: boolean }): void => {
|
||||
deps.refreshDiscordPresence();
|
||||
if (connected) return;
|
||||
if (connected) {
|
||||
deps.syncOverlayMpvSubtitleSuppression();
|
||||
return;
|
||||
}
|
||||
deps.reportJellyfinRemoteStopped();
|
||||
if (!deps.hasInitialJellyfinPlayArg()) return;
|
||||
if (deps.isOverlayRuntimeInitialized()) return;
|
||||
|
||||
@@ -7,7 +7,10 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
class FakeClient {
|
||||
constructor(public socketPath: string, public options: unknown) {}
|
||||
constructor(
|
||||
public socketPath: string,
|
||||
public options: unknown,
|
||||
) {}
|
||||
}
|
||||
|
||||
const build = createBuildMpvClientRuntimeServiceFactoryDepsHandler({
|
||||
@@ -16,7 +19,6 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
|
||||
getResolvedConfig: () => ({ mode: 'test' }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: (visible) => calls.push(`overlay:${visible}`),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => reconnectTimer,
|
||||
setReconnectTimer: (timer) => {
|
||||
@@ -29,7 +31,6 @@ test('mpv runtime service main deps builder maps state and callbacks', () => {
|
||||
const deps = build();
|
||||
assert.equal(deps.socketPath, '/tmp/mpv.sock');
|
||||
assert.equal(deps.options.autoStartOverlay, true);
|
||||
assert.equal(deps.options.shouldBindVisibleOverlayToMpvSubVisibility(), true);
|
||||
assert.equal(deps.options.isVisibleOverlayVisible(), false);
|
||||
assert.deepEqual(deps.options.getResolvedConfig(), { mode: 'test' });
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
getResolvedConfig: () => TResolvedConfig;
|
||||
isAutoStartOverlayEnabled: () => boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
@@ -21,11 +20,10 @@ export function createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
getResolvedConfig: () => deps.getResolvedConfig(),
|
||||
autoStartOverlay: deps.isAutoStartOverlayEnabled(),
|
||||
setOverlayVisible: (visible: boolean) => deps.setOverlayVisible(visible),
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () =>
|
||||
deps.shouldBindVisibleOverlayToMpvSubVisibility(),
|
||||
isVisibleOverlayVisible: () => deps.isVisibleOverlayVisible(),
|
||||
getReconnectTimer: () => deps.getReconnectTimer(),
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => deps.setReconnectTimer(timer),
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) =>
|
||||
deps.setReconnectTimer(timer),
|
||||
},
|
||||
bindEventHandlers: (client: TClient) => deps.bindEventHandlers(client),
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ test('mpv runtime service factory constructs client, binds handlers, and connect
|
||||
getResolvedConfig: () => ({}),
|
||||
autoStartOverlay: true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => false,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
|
||||
@@ -4,7 +4,6 @@ export type MpvClientRuntimeServiceOptions = {
|
||||
getResolvedConfig: () => Config;
|
||||
autoStartOverlay: boolean;
|
||||
setOverlayVisible: (visible: boolean) => void;
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;
|
||||
isVisibleOverlayVisible: () => boolean;
|
||||
getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
|
||||
setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { PartOfSpeech, type SubtitleData } from '../../types';
|
||||
import {
|
||||
HOVER_TOKEN_MESSAGE,
|
||||
HOVER_SCRIPT_NAME,
|
||||
buildHoveredTokenMessageCommand,
|
||||
buildHoveredTokenPayload,
|
||||
createApplyHoveredTokenOverlayHandler,
|
||||
} from './mpv-hover-highlight';
|
||||
|
||||
const SUBTITLE: SubtitleData = {
|
||||
text: '昨日は雨だった。',
|
||||
tokens: [
|
||||
{
|
||||
surface: '昨日',
|
||||
reading: 'きのう',
|
||||
headword: '昨日',
|
||||
startPos: 0,
|
||||
endPos: 2,
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
{
|
||||
surface: 'は',
|
||||
reading: 'は',
|
||||
headword: 'は',
|
||||
startPos: 2,
|
||||
endPos: 3,
|
||||
partOfSpeech: PartOfSpeech.particle,
|
||||
isMerged: false,
|
||||
isKnown: true,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
{
|
||||
surface: '雨',
|
||||
reading: 'あめ',
|
||||
headword: '雨',
|
||||
startPos: 3,
|
||||
endPos: 4,
|
||||
partOfSpeech: PartOfSpeech.noun,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: true,
|
||||
},
|
||||
{
|
||||
surface: 'だった。',
|
||||
reading: 'だった。',
|
||||
headword: 'だ',
|
||||
startPos: 4,
|
||||
endPos: 8,
|
||||
partOfSpeech: PartOfSpeech.other,
|
||||
isMerged: false,
|
||||
isKnown: false,
|
||||
isNPlusOneTarget: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('buildHoveredTokenPayload normalizes metadata and strips empty tokens', () => {
|
||||
const payload = buildHoveredTokenPayload({
|
||||
subtitle: SUBTITLE,
|
||||
hoveredTokenIndex: 2,
|
||||
revision: 5,
|
||||
});
|
||||
|
||||
assert.equal(payload.revision, 5);
|
||||
assert.equal(payload.subtitle, '昨日は雨だった。');
|
||||
assert.equal(payload.hoveredTokenIndex, 2);
|
||||
assert.equal(payload.tokens.length, 4);
|
||||
assert.equal(payload.tokens[0]?.text, '昨日');
|
||||
assert.equal(payload.tokens[0]?.index, 0);
|
||||
assert.equal(payload.tokens[1]?.index, 1);
|
||||
assert.equal(payload.colors.hover, 'C6A0F6');
|
||||
});
|
||||
|
||||
test('buildHoveredTokenPayload normalizes hover color override', () => {
|
||||
const payload = buildHoveredTokenPayload({
|
||||
subtitle: SUBTITLE,
|
||||
hoveredTokenIndex: 1,
|
||||
revision: 7,
|
||||
hoverColor: '#c6a0f6',
|
||||
});
|
||||
|
||||
assert.equal(payload.colors.hover, 'C6A0F6');
|
||||
});
|
||||
|
||||
test('buildHoveredTokenMessageCommand sends script-message-to subminer payload', () => {
|
||||
const payload = buildHoveredTokenPayload({
|
||||
subtitle: SUBTITLE,
|
||||
hoveredTokenIndex: 0,
|
||||
revision: 1,
|
||||
});
|
||||
|
||||
const command = buildHoveredTokenMessageCommand(payload);
|
||||
|
||||
assert.equal(command[0], 'script-message-to');
|
||||
assert.equal(command[1], HOVER_SCRIPT_NAME);
|
||||
assert.equal(command[2], HOVER_TOKEN_MESSAGE);
|
||||
|
||||
const raw = command[3] as string;
|
||||
const parsed = JSON.parse(raw);
|
||||
assert.equal(parsed.revision, 1);
|
||||
assert.equal(parsed.hoveredTokenIndex, 0);
|
||||
assert.equal(parsed.subtitle, '昨日は雨だった。');
|
||||
assert.equal(parsed.tokens.length, 4);
|
||||
});
|
||||
|
||||
test('createApplyHoveredTokenOverlayHandler sends clear payload when hovered token is missing', () => {
|
||||
const commands: Array<(string | number)[]> = [];
|
||||
const apply = createApplyHoveredTokenOverlayHandler({
|
||||
getMpvClient: () => ({
|
||||
connected: true,
|
||||
send: ({ command }: { command: (string | number)[] }) => {
|
||||
commands.push(command);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
getCurrentSubtitleData: () => SUBTITLE,
|
||||
getHoveredTokenIndex: () => null,
|
||||
getHoveredSubtitleRevision: () => 3,
|
||||
getHoverTokenColor: () => null,
|
||||
});
|
||||
|
||||
apply();
|
||||
|
||||
const parsed = JSON.parse(commands[0]?.[3] as string);
|
||||
assert.equal(parsed.hoveredTokenIndex, null);
|
||||
assert.equal(parsed.subtitle, null);
|
||||
assert.equal(parsed.tokens.length, 0);
|
||||
});
|
||||
|
||||
test('createApplyHoveredTokenOverlayHandler sends highlight payload when hover is active', () => {
|
||||
const commands: Array<(string | number)[]> = [];
|
||||
const apply = createApplyHoveredTokenOverlayHandler({
|
||||
getMpvClient: () => ({
|
||||
connected: true,
|
||||
send: ({ command }: { command: (string | number)[] }) => {
|
||||
commands.push(command);
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
getCurrentSubtitleData: () => SUBTITLE,
|
||||
getHoveredTokenIndex: () => 0,
|
||||
getHoveredSubtitleRevision: () => 3,
|
||||
getHoverTokenColor: () => '#c6a0f6',
|
||||
});
|
||||
|
||||
apply();
|
||||
|
||||
const parsed = JSON.parse(commands[0]?.[3] as string);
|
||||
assert.equal(parsed.hoveredTokenIndex, 0);
|
||||
assert.equal(parsed.subtitle, '昨日は雨だった。');
|
||||
assert.equal(parsed.tokens.length, 4);
|
||||
assert.equal(parsed.colors.hover, 'C6A0F6');
|
||||
assert.equal(commands[0]?.[0], 'script-message-to');
|
||||
assert.equal(commands[0]?.[1], HOVER_SCRIPT_NAME);
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { SubtitleData } from '../../types';
|
||||
|
||||
export const HOVER_SCRIPT_NAME = 'subminer';
|
||||
export const HOVER_TOKEN_MESSAGE = 'subminer-hover-token';
|
||||
|
||||
const DEFAULT_HOVER_TOKEN_COLOR = 'C6A0F6';
|
||||
const DEFAULT_TOKEN_COLOR = 'FFFFFF';
|
||||
|
||||
export type HoverPayloadToken = {
|
||||
text: string;
|
||||
index: number;
|
||||
startPos: number | null;
|
||||
endPos: number | null;
|
||||
};
|
||||
|
||||
export type HoverTokenPayload = {
|
||||
revision: number;
|
||||
subtitle: string | null;
|
||||
hoveredTokenIndex: number | null;
|
||||
tokens: HoverPayloadToken[];
|
||||
colors: {
|
||||
base: string;
|
||||
hover: string;
|
||||
};
|
||||
};
|
||||
|
||||
type HoverTokenInput = {
|
||||
subtitle: SubtitleData | null;
|
||||
hoveredTokenIndex: number | null;
|
||||
revision: number;
|
||||
hoverColor?: string | null;
|
||||
};
|
||||
|
||||
function normalizeHexColor(color: string | null | undefined, fallback: string): string {
|
||||
if (typeof color !== 'string') {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = color.trim().replace(/^#/, '').toUpperCase();
|
||||
return /^[0-9A-F]{6}$/.test(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
function sanitizeSubtitleText(text: string): string {
|
||||
return text
|
||||
.replace(/\\N/g, '\n')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\{[^}]*\}/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeTokenSurface(surface: unknown): string {
|
||||
return typeof surface === 'string' ? surface : '';
|
||||
}
|
||||
|
||||
function hasHoveredToken(subtitle: SubtitleData | null, hoveredTokenIndex: number | null): boolean {
|
||||
if (!subtitle || hoveredTokenIndex === null || hoveredTokenIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return subtitle.tokens?.some((token, index) => index === hoveredTokenIndex) ?? false;
|
||||
}
|
||||
|
||||
export function buildHoveredTokenPayload(input: HoverTokenInput): HoverTokenPayload {
|
||||
const { subtitle, hoveredTokenIndex, revision, hoverColor } = input;
|
||||
|
||||
const tokens: HoverPayloadToken[] = [];
|
||||
|
||||
if (subtitle?.tokens && subtitle.tokens.length > 0) {
|
||||
for (let tokenIndex = 0; tokenIndex < subtitle.tokens.length; tokenIndex += 1) {
|
||||
const token = subtitle.tokens[tokenIndex];
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const surface = sanitizeTokenSurface(token?.surface);
|
||||
if (!surface || surface.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
text: surface,
|
||||
index: tokenIndex,
|
||||
startPos: Number.isFinite(token.startPos) ? token.startPos : null,
|
||||
endPos: Number.isFinite(token.endPos) ? token.endPos : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
revision,
|
||||
subtitle: subtitle ? sanitizeSubtitleText(subtitle.text) : null,
|
||||
hoveredTokenIndex:
|
||||
hoveredTokenIndex !== null && hoveredTokenIndex >= 0 ? hoveredTokenIndex : null,
|
||||
tokens,
|
||||
colors: {
|
||||
base: DEFAULT_TOKEN_COLOR,
|
||||
hover: normalizeHexColor(hoverColor, DEFAULT_HOVER_TOKEN_COLOR),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHoveredTokenMessageCommand(payload: HoverTokenPayload): (string | number)[] {
|
||||
return [
|
||||
'script-message-to',
|
||||
HOVER_SCRIPT_NAME,
|
||||
HOVER_TOKEN_MESSAGE,
|
||||
JSON.stringify(payload),
|
||||
];
|
||||
}
|
||||
|
||||
export function createApplyHoveredTokenOverlayHandler(deps: {
|
||||
getMpvClient: () => {
|
||||
connected: boolean;
|
||||
send: (payload: { command: (string | number)[] }) => boolean;
|
||||
} | null;
|
||||
getCurrentSubtitleData: () => SubtitleData | null;
|
||||
getHoveredTokenIndex: () => number | null;
|
||||
getHoveredSubtitleRevision: () => number;
|
||||
getHoverTokenColor: () => string | null;
|
||||
}) {
|
||||
return (): void => {
|
||||
const mpvClient = deps.getMpvClient();
|
||||
if (!mpvClient || !mpvClient.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subtitle = deps.getCurrentSubtitleData();
|
||||
const hoveredTokenIndex = deps.getHoveredTokenIndex();
|
||||
const revision = deps.getHoveredSubtitleRevision();
|
||||
const hoverColor = deps.getHoverTokenColor();
|
||||
const payload = buildHoveredTokenPayload({
|
||||
subtitle: subtitle && hasHoveredToken(subtitle, hoveredTokenIndex) ? subtitle : null,
|
||||
hoveredTokenIndex: hoveredTokenIndex,
|
||||
revision,
|
||||
hoverColor,
|
||||
});
|
||||
|
||||
mpvClient.send({ command: buildHoveredTokenMessageCommand(payload) });
|
||||
};
|
||||
}
|
||||
@@ -15,9 +15,7 @@ export function createBuildApplyJellyfinMpvDefaultsMainDepsHandler(
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildGetDefaultSocketPathMainDepsHandler(
|
||||
deps: GetDefaultSocketPathMainDeps,
|
||||
) {
|
||||
export function createBuildGetDefaultSocketPathMainDepsHandler(deps: GetDefaultSocketPathMainDeps) {
|
||||
return (): GetDefaultSocketPathMainDeps => ({
|
||||
platform: deps.platform,
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@ test('media path change handler reports stop for empty path and probes media key
|
||||
const handler = createHandleMpvMediaPathChangeHandler({
|
||||
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||
reportJellyfinRemoteStopped: () => calls.push('stopped'),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
getCurrentAnilistMediaKey: () => 'show:1',
|
||||
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`),
|
||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||
@@ -63,6 +64,7 @@ test('media path change handler reports stop for empty path and probes media key
|
||||
assert.deepEqual(calls, [
|
||||
'path:',
|
||||
'stopped',
|
||||
'restore-mpv-sub',
|
||||
'reset:show:1',
|
||||
'probe:show:1',
|
||||
'guess:show:1',
|
||||
|
||||
@@ -33,6 +33,7 @@ export function createHandleMpvSecondarySubtitleChangeHandler(deps: {
|
||||
export function createHandleMpvMediaPathChangeHandler(deps: {
|
||||
updateCurrentMediaPath: (path: string) => void;
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
restoreMpvSubVisibility: () => void;
|
||||
getCurrentAnilistMediaKey: () => string | null;
|
||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||
@@ -44,6 +45,7 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
||||
deps.updateCurrentMediaPath(path);
|
||||
if (!path) {
|
||||
deps.reportJellyfinRemoteStopped();
|
||||
deps.restoreMpvSubVisibility();
|
||||
}
|
||||
const mediaKey = deps.getCurrentAnilistMediaKey();
|
||||
deps.resetAnilistMediaTracking(mediaKey);
|
||||
|
||||
@@ -8,6 +8,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
||||
|
||||
const bind = createBindMpvMainEventHandlersHandler({
|
||||
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||
hasInitialJellyfinPlayArg: () => false,
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
isQuitOnDisconnectArmed: () => false,
|
||||
@@ -35,6 +36,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
||||
broadcastSecondarySubtitle: (text) => calls.push(`broadcast-secondary:${text}`),
|
||||
|
||||
updateCurrentMediaPath: (path) => calls.push(`media-path:${path}`),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
getCurrentAnilistMediaKey: () => 'media-key',
|
||||
resetAnilistMediaTracking: (key) => calls.push(`reset-media:${String(key)}`),
|
||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||
@@ -62,6 +64,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
||||
});
|
||||
|
||||
handlers.get('subtitle-change')?.({ text: 'line' });
|
||||
handlers.get('media-path-change')?.({ path: '' });
|
||||
handlers.get('media-title-change')?.({ title: 'Episode 1' });
|
||||
handlers.get('time-pos-change')?.({ time: 2.5 });
|
||||
handlers.get('pause-change')?.({ paused: true });
|
||||
@@ -70,6 +73,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
||||
assert.ok(calls.includes('broadcast-sub:line'));
|
||||
assert.ok(calls.includes('subtitle-change:line'));
|
||||
assert.ok(calls.includes('media-title:Episode 1'));
|
||||
assert.ok(calls.includes('restore-mpv-sub'));
|
||||
assert.ok(calls.includes('reset-guess-state'));
|
||||
assert.ok(calls.includes('notify-title:Episode 1'));
|
||||
assert.ok(calls.includes('progress:normal'));
|
||||
|
||||
@@ -19,6 +19,7 @@ type MpvEventClient = Parameters<ReturnType<typeof createBindMpvClientEventHandl
|
||||
|
||||
export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
hasInitialJellyfinPlayArg: () => boolean;
|
||||
isOverlayRuntimeInitialized: () => boolean;
|
||||
isQuitOnDisconnectArmed: () => boolean;
|
||||
@@ -42,6 +43,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
broadcastSecondarySubtitle: (text: string) => void;
|
||||
|
||||
updateCurrentMediaPath: (path: string) => void;
|
||||
restoreMpvSubVisibility: () => void;
|
||||
getCurrentAnilistMediaKey: () => string | null;
|
||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||
@@ -63,6 +65,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
const handleMpvConnectionChange = createHandleMpvConnectionChangeHandler({
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
hasInitialJellyfinPlayArg: () => deps.hasInitialJellyfinPlayArg(),
|
||||
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
|
||||
isQuitOnDisconnectArmed: () => deps.isQuitOnDisconnectArmed(),
|
||||
@@ -94,6 +97,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
||||
const handleMpvMediaPathChange = createHandleMpvMediaPathChangeHandler({
|
||||
updateCurrentMediaPath: (path) => deps.updateCurrentMediaPath(path),
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||
resetAnilistMediaTracking: (mediaKey) => deps.resetAnilistMediaTracking(mediaKey),
|
||||
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
||||
|
||||
@@ -32,6 +32,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
},
|
||||
quitApp: () => calls.push('quit'),
|
||||
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
||||
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
||||
maybeRunAnilistPostWatchUpdate: async () => {
|
||||
calls.push('anilist-post-watch');
|
||||
},
|
||||
@@ -39,7 +40,9 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
broadcastToOverlayWindows: (channel, payload) =>
|
||||
calls.push(`broadcast:${channel}:${String(payload)}`),
|
||||
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
|
||||
ensureImmersionTrackerInitialized: () => calls.push('ensure-immersion'),
|
||||
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||
getCurrentAnilistMediaKey: () => 'media-key',
|
||||
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`),
|
||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||
@@ -59,6 +62,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
deps.scheduleQuitCheck(() => calls.push('scheduled-callback'));
|
||||
deps.quitApp();
|
||||
deps.reportJellyfinRemoteStopped();
|
||||
deps.syncOverlayMpvSubtitleSuppression();
|
||||
deps.recordImmersionSubtitleLine('x', 0, 1);
|
||||
assert.equal(deps.hasSubtitleTimingTracker(), true);
|
||||
deps.recordSubtitleTiming('y', 0, 1);
|
||||
@@ -72,6 +76,7 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
deps.broadcastSubtitleAss('ass');
|
||||
deps.broadcastSecondarySubtitle('sec');
|
||||
deps.updateCurrentMediaPath('/tmp/video');
|
||||
deps.restoreMpvSubVisibility();
|
||||
assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key');
|
||||
deps.resetAnilistMediaTracking('media-key');
|
||||
deps.maybeProbeAnilistDuration('media-key');
|
||||
@@ -91,8 +96,11 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
||||
assert.equal(appState.playbackPaused, true);
|
||||
assert.equal(appState.previousSecondarySubVisibility, true);
|
||||
assert.ok(calls.includes('remote-stopped'));
|
||||
assert.ok(calls.includes('sync-overlay-mpv-sub'));
|
||||
assert.ok(calls.includes('anilist-post-watch'));
|
||||
assert.ok(calls.includes('ensure-immersion'));
|
||||
assert.ok(calls.includes('sync-immersion'));
|
||||
assert.ok(calls.includes('metrics'));
|
||||
assert.ok(calls.includes('presence-refresh'));
|
||||
assert.ok(calls.includes('restore-mpv-sub'));
|
||||
});
|
||||
|
||||
@@ -21,11 +21,13 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
scheduleQuitCheck: (callback: () => void) => void;
|
||||
quitApp: () => void;
|
||||
reportJellyfinRemoteStopped: () => void;
|
||||
syncOverlayMpvSubtitleSuppression: () => void;
|
||||
maybeRunAnilistPostWatchUpdate: () => Promise<void>;
|
||||
logSubtitleTimingError: (message: string, error: unknown) => void;
|
||||
broadcastToOverlayWindows: (channel: string, payload: unknown) => void;
|
||||
onSubtitleChange: (text: string) => void;
|
||||
updateCurrentMediaPath: (path: string) => void;
|
||||
restoreMpvSubVisibility: () => void;
|
||||
getCurrentAnilistMediaKey: () => string | null;
|
||||
resetAnilistMediaTracking: (mediaKey: string | null) => void;
|
||||
maybeProbeAnilistDuration: (mediaKey: string) => void;
|
||||
@@ -36,17 +38,21 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) => void;
|
||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) => void;
|
||||
refreshDiscordPresence: () => void;
|
||||
ensureImmersionTrackerInitialized: () => void;
|
||||
}) {
|
||||
return () => ({
|
||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||
hasInitialJellyfinPlayArg: () => Boolean(deps.appState.initialArgs?.jellyfinPlay),
|
||||
isOverlayRuntimeInitialized: () => deps.appState.overlayRuntimeInitialized,
|
||||
isQuitOnDisconnectArmed: () => deps.getQuitOnDisconnectArmed(),
|
||||
scheduleQuitCheck: (callback: () => void) => deps.scheduleQuitCheck(callback),
|
||||
isMpvConnected: () => Boolean(deps.appState.mpvClient?.connected),
|
||||
quitApp: () => deps.quitApp(),
|
||||
recordImmersionSubtitleLine: (text: string, start: number, end: number) =>
|
||||
deps.appState.immersionTracker?.recordSubtitleLine?.(text, start, end),
|
||||
recordImmersionSubtitleLine: (text: string, start: number, end: number) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordSubtitleLine?.(text, start, end);
|
||||
},
|
||||
hasSubtitleTimingTracker: () => Boolean(deps.appState.subtitleTimingTracker),
|
||||
recordSubtitleTiming: (text: string, start: number, end: number) =>
|
||||
deps.appState.subtitleTimingTracker?.recordSubtitle?.(text, start, end),
|
||||
@@ -68,6 +74,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
broadcastSecondarySubtitle: (text: string) =>
|
||||
deps.broadcastToOverlayWindows('secondary-subtitle:set', text),
|
||||
updateCurrentMediaPath: (path: string) => deps.updateCurrentMediaPath(path),
|
||||
restoreMpvSubVisibility: () => deps.restoreMpvSubVisibility(),
|
||||
getCurrentAnilistMediaKey: () => deps.getCurrentAnilistMediaKey(),
|
||||
resetAnilistMediaTracking: (mediaKey: string | null) =>
|
||||
deps.resetAnilistMediaTracking(mediaKey),
|
||||
@@ -76,14 +83,19 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
||||
syncImmersionMediaState: () => deps.syncImmersionMediaState(),
|
||||
updateCurrentMediaTitle: (title: string) => deps.updateCurrentMediaTitle(title),
|
||||
resetAnilistMediaGuessState: () => deps.resetAnilistMediaGuessState(),
|
||||
notifyImmersionTitleUpdate: (title: string) =>
|
||||
deps.appState.immersionTracker?.handleMediaTitleUpdate?.(title),
|
||||
recordPlaybackPosition: (time: number) =>
|
||||
deps.appState.immersionTracker?.recordPlaybackPosition?.(time),
|
||||
notifyImmersionTitleUpdate: (title: string) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.handleMediaTitleUpdate?.(title);
|
||||
},
|
||||
recordPlaybackPosition: (time: number) => {
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordPlaybackPosition?.(time);
|
||||
},
|
||||
reportJellyfinRemoteProgress: (forceImmediate: boolean) =>
|
||||
deps.reportJellyfinRemoteProgress(forceImmediate),
|
||||
recordPauseState: (paused: boolean) => {
|
||||
deps.appState.playbackPaused = paused;
|
||||
deps.ensureImmersionTrackerInitialized();
|
||||
deps.appState.immersionTracker?.recordPauseState?.(paused);
|
||||
},
|
||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) =>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { createUpdateMpvSubtitleRenderMetricsHandler } from './mpv-subtitle-render-metrics';
|
||||
|
||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<typeof createUpdateMpvSubtitleRenderMetricsHandler>[0];
|
||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
|
||||
typeof createUpdateMpvSubtitleRenderMetricsHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
|
||||
deps: UpdateMpvSubtitleRenderMetricsMainDeps,
|
||||
|
||||
@@ -22,7 +22,10 @@ test('numeric shortcut runtime main deps builder maps callbacks', () => {
|
||||
},
|
||||
})();
|
||||
|
||||
assert.equal(deps.globalShortcut.register('1', () => {}), true);
|
||||
assert.equal(
|
||||
deps.globalShortcut.register('1', () => {}),
|
||||
true,
|
||||
);
|
||||
deps.globalShortcut.unregister('1');
|
||||
deps.showMpvOsd('x');
|
||||
deps.setTimer(() => calls.push('tick'), 1000);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { NumericShortcutRuntimeOptions } from '../../core/services/numeric-shortcut';
|
||||
|
||||
export function createBuildNumericShortcutRuntimeMainDepsHandler(deps: NumericShortcutRuntimeOptions) {
|
||||
export function createBuildNumericShortcutRuntimeMainDepsHandler(
|
||||
deps: NumericShortcutRuntimeOptions,
|
||||
) {
|
||||
return (): NumericShortcutRuntimeOptions => ({
|
||||
globalShortcut: deps.globalShortcut,
|
||||
showMpvOsd: (text: string) => deps.showMpvOsd(text),
|
||||
|
||||
@@ -3,8 +3,12 @@ import type {
|
||||
createStartNumericShortcutSessionHandler,
|
||||
} from './numeric-shortcut-session-handlers';
|
||||
|
||||
type CancelNumericShortcutSessionMainDeps = Parameters<typeof createCancelNumericShortcutSessionHandler>[0];
|
||||
type StartNumericShortcutSessionMainDeps = Parameters<typeof createStartNumericShortcutSessionHandler>[0];
|
||||
type CancelNumericShortcutSessionMainDeps = Parameters<
|
||||
typeof createCancelNumericShortcutSessionHandler
|
||||
>[0];
|
||||
type StartNumericShortcutSessionMainDeps = Parameters<
|
||||
typeof createStartNumericShortcutSessionHandler
|
||||
>[0];
|
||||
|
||||
export function createBuildCancelNumericShortcutSessionMainDepsHandler(
|
||||
deps: CancelNumericShortcutSessionMainDeps,
|
||||
|
||||
@@ -20,8 +20,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
|
||||
const cancelPendingMultiCopyMainDeps = createBuildCancelNumericShortcutSessionMainDepsHandler({
|
||||
session: deps.multiCopySession,
|
||||
})();
|
||||
const cancelPendingMultiCopyHandler =
|
||||
createCancelNumericShortcutSessionHandler(cancelPendingMultiCopyMainDeps);
|
||||
const cancelPendingMultiCopyHandler = createCancelNumericShortcutSessionHandler(
|
||||
cancelPendingMultiCopyMainDeps,
|
||||
);
|
||||
|
||||
const startPendingMultiCopyMainDeps = createBuildStartNumericShortcutSessionMainDepsHandler({
|
||||
session: deps.multiCopySession,
|
||||
@@ -32,8 +33,9 @@ export function createNumericShortcutSessionRuntimeHandlers(deps: {
|
||||
cancelled: 'Cancelled',
|
||||
},
|
||||
})();
|
||||
const startPendingMultiCopyHandler =
|
||||
createStartNumericShortcutSessionHandler(startPendingMultiCopyMainDeps);
|
||||
const startPendingMultiCopyHandler = createStartNumericShortcutSessionHandler(
|
||||
startPendingMultiCopyMainDeps,
|
||||
);
|
||||
|
||||
const cancelPendingMineSentenceMultipleMainDeps =
|
||||
createBuildCancelNumericShortcutSessionMainDepsHandler({
|
||||
|
||||
@@ -19,12 +19,10 @@ test('overlay content measurement store main deps builder maps callbacks', () =>
|
||||
|
||||
test('overlay modal runtime main deps builder maps window resolvers', () => {
|
||||
const mainWindow = { id: 'main' };
|
||||
const invisibleWindow = { id: 'invisible' };
|
||||
const modalWindow = { id: 'modal' };
|
||||
const calls: string[] = [];
|
||||
const deps = createBuildOverlayModalRuntimeMainDepsHandler({
|
||||
getMainWindow: () => mainWindow as never,
|
||||
getInvisibleWindow: () => invisibleWindow as never,
|
||||
getModalWindow: () => modalWindow as never,
|
||||
createModalWindow: () => modalWindow as never,
|
||||
getModalGeometry: () => ({ x: 1, y: 2, width: 3, height: 4 }),
|
||||
@@ -33,7 +31,6 @@ test('overlay modal runtime main deps builder maps window resolvers', () => {
|
||||
})();
|
||||
|
||||
assert.equal(deps.getMainWindow(), mainWindow);
|
||||
assert.equal(deps.getInvisibleWindow(), invisibleWindow);
|
||||
assert.equal(deps.getModalWindow(), modalWindow);
|
||||
assert.equal(deps.createModalWindow(), modalWindow);
|
||||
assert.deepEqual(deps.getModalGeometry(), { x: 1, y: 2, width: 3, height: 4 });
|
||||
|
||||
@@ -14,12 +14,9 @@ export function createBuildOverlayContentMeasurementStoreMainDepsHandler(
|
||||
});
|
||||
}
|
||||
|
||||
export function createBuildOverlayModalRuntimeMainDepsHandler(
|
||||
deps: OverlayWindowResolver,
|
||||
) {
|
||||
export function createBuildOverlayModalRuntimeMainDepsHandler(deps: OverlayWindowResolver) {
|
||||
return (): OverlayWindowResolver => ({
|
||||
getMainWindow: () => deps.getMainWindow(),
|
||||
getInvisibleWindow: () => deps.getInvisibleWindow(),
|
||||
getModalWindow: () => deps.getModalWindow(),
|
||||
createModalWindow: () => deps.createModalWindow(),
|
||||
getModalGeometry: () => deps.getModalGeometry(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user