mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-02 18:22:42 -08:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
40
src/main/runtime/composers/anilist-setup-composer.test.ts
Normal file
40
src/main/runtime/composers/anilist-setup-composer.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { composeAnilistSetupHandlers } from './anilist-setup-composer';
|
||||
|
||||
test('composeAnilistSetupHandlers returns callable setup handlers', () => {
|
||||
const composed = composeAnilistSetupHandlers({
|
||||
notifyDeps: {
|
||||
hasMpvClient: () => false,
|
||||
showMpvOsd: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
logInfo: () => {},
|
||||
},
|
||||
consumeTokenDeps: {
|
||||
consumeAnilistSetupCallbackUrl: () => false,
|
||||
saveToken: () => {},
|
||||
setCachedToken: () => {},
|
||||
setResolvedState: () => {},
|
||||
setSetupPageOpened: () => {},
|
||||
onSuccess: () => {},
|
||||
closeWindow: () => {},
|
||||
},
|
||||
handleProtocolDeps: {
|
||||
consumeAnilistSetupTokenFromUrl: () => false,
|
||||
logWarn: () => {},
|
||||
},
|
||||
registerProtocolClientDeps: {
|
||||
isDefaultApp: () => false,
|
||||
getArgv: () => [],
|
||||
execPath: process.execPath,
|
||||
resolvePath: (value) => value,
|
||||
setAsDefaultProtocolClient: () => true,
|
||||
logWarn: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.notifyAnilistSetup, 'function');
|
||||
assert.equal(typeof composed.consumeAnilistSetupTokenFromUrl, 'function');
|
||||
assert.equal(typeof composed.handleAnilistSetupProtocolUrl, 'function');
|
||||
assert.equal(typeof composed.registerSubminerProtocolClient, 'function');
|
||||
});
|
||||
56
src/main/runtime/composers/anilist-setup-composer.ts
Normal file
56
src/main/runtime/composers/anilist-setup-composer.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler,
|
||||
createBuildHandleAnilistSetupProtocolUrlMainDepsHandler,
|
||||
createBuildNotifyAnilistSetupMainDepsHandler,
|
||||
createBuildRegisterSubminerProtocolClientMainDepsHandler,
|
||||
createConsumeAnilistSetupTokenFromUrlHandler,
|
||||
createHandleAnilistSetupProtocolUrlHandler,
|
||||
createNotifyAnilistSetupHandler,
|
||||
createRegisterSubminerProtocolClientHandler,
|
||||
} from '../domains/anilist';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type NotifyHandler = ReturnType<typeof createNotifyAnilistSetupHandler>;
|
||||
type ConsumeHandler = ReturnType<typeof createConsumeAnilistSetupTokenFromUrlHandler>;
|
||||
type HandleProtocolHandler = ReturnType<typeof createHandleAnilistSetupProtocolUrlHandler>;
|
||||
type RegisterClientHandler = ReturnType<typeof createRegisterSubminerProtocolClientHandler>;
|
||||
|
||||
export type AnilistSetupComposerOptions = ComposerInputs<{
|
||||
notifyDeps: Parameters<typeof createBuildNotifyAnilistSetupMainDepsHandler>[0];
|
||||
consumeTokenDeps: Parameters<typeof createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler>[0];
|
||||
handleProtocolDeps: Parameters<typeof createBuildHandleAnilistSetupProtocolUrlMainDepsHandler>[0];
|
||||
registerProtocolClientDeps: Parameters<
|
||||
typeof createBuildRegisterSubminerProtocolClientMainDepsHandler
|
||||
>[0];
|
||||
}>;
|
||||
|
||||
export type AnilistSetupComposerResult = ComposerOutputs<{
|
||||
notifyAnilistSetup: NotifyHandler;
|
||||
consumeAnilistSetupTokenFromUrl: ConsumeHandler;
|
||||
handleAnilistSetupProtocolUrl: HandleProtocolHandler;
|
||||
registerSubminerProtocolClient: RegisterClientHandler;
|
||||
}>;
|
||||
|
||||
export function composeAnilistSetupHandlers(
|
||||
options: AnilistSetupComposerOptions,
|
||||
): AnilistSetupComposerResult {
|
||||
const notifyAnilistSetup = createNotifyAnilistSetupHandler(
|
||||
createBuildNotifyAnilistSetupMainDepsHandler(options.notifyDeps)(),
|
||||
);
|
||||
const consumeAnilistSetupTokenFromUrl = createConsumeAnilistSetupTokenFromUrlHandler(
|
||||
createBuildConsumeAnilistSetupTokenFromUrlMainDepsHandler(options.consumeTokenDeps)(),
|
||||
);
|
||||
const handleAnilistSetupProtocolUrl = createHandleAnilistSetupProtocolUrlHandler(
|
||||
createBuildHandleAnilistSetupProtocolUrlMainDepsHandler(options.handleProtocolDeps)(),
|
||||
);
|
||||
const registerSubminerProtocolClient = createRegisterSubminerProtocolClientHandler(
|
||||
createBuildRegisterSubminerProtocolClientMainDepsHandler(options.registerProtocolClientDeps)(),
|
||||
);
|
||||
|
||||
return {
|
||||
notifyAnilistSetup,
|
||||
consumeAnilistSetupTokenFromUrl,
|
||||
handleAnilistSetupProtocolUrl,
|
||||
registerSubminerProtocolClient,
|
||||
};
|
||||
}
|
||||
237
src/main/runtime/composers/anilist-tracking-composer.test.ts
Normal file
237
src/main/runtime/composers/anilist-tracking-composer.test.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { AnilistMediaGuess } from '../../../core/services/anilist/anilist-updater';
|
||||
import { composeAnilistTrackingHandlers } from './anilist-tracking-composer';
|
||||
|
||||
test('composeAnilistTrackingHandlers returns callable handlers and forwards calls to deps', async () => {
|
||||
const refreshSavedTokens: string[] = [];
|
||||
let refreshCachedToken: string | null = null;
|
||||
|
||||
let mediaKeyState: string | null = 'media-key';
|
||||
let mediaDurationSecState: number | null = null;
|
||||
let mediaGuessState: AnilistMediaGuess | null = null;
|
||||
let mediaGuessPromiseState: Promise<AnilistMediaGuess | null> | null = null;
|
||||
let lastDurationProbeAtMsState = 0;
|
||||
let requestMpvDurationCalls = 0;
|
||||
let guessAnilistMediaInfoCalls = 0;
|
||||
|
||||
let retryUpdateCalls = 0;
|
||||
let maybeRunUpdateCalls = 0;
|
||||
|
||||
const composed = composeAnilistTrackingHandlers({
|
||||
refreshClientSecretMainDeps: {
|
||||
getResolvedConfig: () => ({ anilist: { accessToken: 'refresh-token' } }),
|
||||
isAnilistTrackingEnabled: () => true,
|
||||
getCachedAccessToken: () => refreshCachedToken,
|
||||
setCachedAccessToken: (token) => {
|
||||
refreshCachedToken = token;
|
||||
},
|
||||
saveStoredToken: (token) => {
|
||||
refreshSavedTokens.push(token);
|
||||
},
|
||||
loadStoredToken: () => null,
|
||||
setClientSecretState: () => {},
|
||||
getAnilistSetupPageOpened: () => false,
|
||||
setAnilistSetupPageOpened: () => {},
|
||||
openAnilistSetupWindow: () => {},
|
||||
now: () => 100,
|
||||
},
|
||||
getCurrentMediaKeyMainDeps: {
|
||||
getCurrentMediaPath: () => ' media-key ',
|
||||
},
|
||||
resetMediaTrackingMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
mediaKeyState = value;
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
mediaDurationSecState = value;
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
lastDurationProbeAtMsState = value;
|
||||
},
|
||||
},
|
||||
getMediaGuessRuntimeStateMainDeps: {
|
||||
getMediaKey: () => mediaKeyState,
|
||||
getMediaDurationSec: () => mediaDurationSecState,
|
||||
getMediaGuess: () => mediaGuessState,
|
||||
getMediaGuessPromise: () => mediaGuessPromiseState,
|
||||
getLastDurationProbeAtMs: () => lastDurationProbeAtMsState,
|
||||
},
|
||||
setMediaGuessRuntimeStateMainDeps: {
|
||||
setMediaKey: (value) => {
|
||||
mediaKeyState = value;
|
||||
},
|
||||
setMediaDurationSec: (value) => {
|
||||
mediaDurationSecState = value;
|
||||
},
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
setLastDurationProbeAtMs: (value) => {
|
||||
lastDurationProbeAtMsState = value;
|
||||
},
|
||||
},
|
||||
resetMediaGuessStateMainDeps: {
|
||||
setMediaGuess: (value) => {
|
||||
mediaGuessState = value;
|
||||
},
|
||||
setMediaGuessPromise: (value) => {
|
||||
mediaGuessPromiseState = value;
|
||||
},
|
||||
},
|
||||
maybeProbeDurationMainDeps: {
|
||||
getState: () => ({
|
||||
mediaKey: mediaKeyState,
|
||||
mediaDurationSec: mediaDurationSecState,
|
||||
mediaGuess: mediaGuessState,
|
||||
mediaGuessPromise: mediaGuessPromiseState,
|
||||
lastDurationProbeAtMs: lastDurationProbeAtMsState,
|
||||
}),
|
||||
setState: (state) => {
|
||||
mediaKeyState = state.mediaKey;
|
||||
mediaDurationSecState = state.mediaDurationSec;
|
||||
mediaGuessState = state.mediaGuess;
|
||||
mediaGuessPromiseState = state.mediaGuessPromise;
|
||||
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
|
||||
},
|
||||
durationRetryIntervalMs: 0,
|
||||
now: () => 1000,
|
||||
requestMpvDuration: async () => {
|
||||
requestMpvDurationCalls += 1;
|
||||
return 120;
|
||||
},
|
||||
logWarn: () => {},
|
||||
},
|
||||
ensureMediaGuessMainDeps: {
|
||||
getState: () => ({
|
||||
mediaKey: mediaKeyState,
|
||||
mediaDurationSec: mediaDurationSecState,
|
||||
mediaGuess: mediaGuessState,
|
||||
mediaGuessPromise: mediaGuessPromiseState,
|
||||
lastDurationProbeAtMs: lastDurationProbeAtMsState,
|
||||
}),
|
||||
setState: (state) => {
|
||||
mediaKeyState = state.mediaKey;
|
||||
mediaDurationSecState = state.mediaDurationSec;
|
||||
mediaGuessState = state.mediaGuess;
|
||||
mediaGuessPromiseState = state.mediaGuessPromise;
|
||||
lastDurationProbeAtMsState = state.lastDurationProbeAtMs;
|
||||
},
|
||||
resolveMediaPathForJimaku: (value) => value,
|
||||
getCurrentMediaPath: () => '/tmp/media.mkv',
|
||||
getCurrentMediaTitle: () => 'Episode title',
|
||||
guessAnilistMediaInfo: async () => {
|
||||
guessAnilistMediaInfoCalls += 1;
|
||||
return { title: 'Episode title', episode: 7, source: 'guessit' };
|
||||
},
|
||||
},
|
||||
processNextRetryUpdateMainDeps: {
|
||||
nextReady: () => ({ key: 'retry-key', title: 'Retry title', episode: 1 }),
|
||||
refreshRetryQueueState: () => {},
|
||||
setLastAttemptAt: () => {},
|
||||
setLastError: () => {},
|
||||
refreshAnilistClientSecretState: async () => 'retry-token',
|
||||
updateAnilistPostWatchProgress: async () => {
|
||||
retryUpdateCalls += 1;
|
||||
return { status: 'updated', message: 'ok' };
|
||||
},
|
||||
markSuccess: () => {},
|
||||
rememberAttemptedUpdateKey: () => {},
|
||||
markFailure: () => {},
|
||||
logInfo: () => {},
|
||||
now: () => 1,
|
||||
},
|
||||
maybeRunPostWatchUpdateMainDeps: {
|
||||
getInFlight: () => false,
|
||||
setInFlight: () => {},
|
||||
getResolvedConfig: () => ({ tracking: true }),
|
||||
isAnilistTrackingEnabled: () => true,
|
||||
getCurrentMediaKey: () => 'media-key',
|
||||
hasMpvClient: () => true,
|
||||
getTrackedMediaKey: () => 'media-key',
|
||||
resetTrackedMedia: () => {},
|
||||
getWatchedSeconds: () => 500,
|
||||
maybeProbeAnilistDuration: async () => 600,
|
||||
ensureAnilistMediaGuess: async () => ({
|
||||
title: 'Episode title',
|
||||
episode: 2,
|
||||
source: 'guessit',
|
||||
}),
|
||||
hasAttemptedUpdateKey: () => false,
|
||||
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
|
||||
refreshAnilistClientSecretState: async () => 'run-token',
|
||||
enqueueRetry: () => {},
|
||||
markRetryFailure: () => {},
|
||||
markRetrySuccess: () => {},
|
||||
refreshRetryQueueState: () => {},
|
||||
updateAnilistPostWatchProgress: async () => {
|
||||
maybeRunUpdateCalls += 1;
|
||||
return { status: 'updated', message: 'updated from maybeRun' };
|
||||
},
|
||||
rememberAttemptedUpdateKey: () => {},
|
||||
showMpvOsd: () => {},
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
minWatchSeconds: 10,
|
||||
minWatchRatio: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.refreshAnilistClientSecretState, 'function');
|
||||
assert.equal(typeof composed.getCurrentAnilistMediaKey, 'function');
|
||||
assert.equal(typeof composed.resetAnilistMediaTracking, 'function');
|
||||
assert.equal(typeof composed.getAnilistMediaGuessRuntimeState, 'function');
|
||||
assert.equal(typeof composed.setAnilistMediaGuessRuntimeState, 'function');
|
||||
assert.equal(typeof composed.resetAnilistMediaGuessState, 'function');
|
||||
assert.equal(typeof composed.maybeProbeAnilistDuration, 'function');
|
||||
assert.equal(typeof composed.ensureAnilistMediaGuess, 'function');
|
||||
assert.equal(typeof composed.processNextAnilistRetryUpdate, 'function');
|
||||
assert.equal(typeof composed.maybeRunAnilistPostWatchUpdate, 'function');
|
||||
|
||||
const refreshed = await composed.refreshAnilistClientSecretState({ force: true });
|
||||
assert.equal(refreshed, 'refresh-token');
|
||||
assert.deepEqual(refreshSavedTokens, ['refresh-token']);
|
||||
|
||||
assert.equal(composed.getCurrentAnilistMediaKey(), 'media-key');
|
||||
composed.resetAnilistMediaTracking('next-key');
|
||||
assert.equal(mediaKeyState, 'next-key');
|
||||
assert.equal(mediaDurationSecState, null);
|
||||
|
||||
composed.setAnilistMediaGuessRuntimeState({
|
||||
mediaKey: 'media-key',
|
||||
mediaDurationSec: 90,
|
||||
mediaGuess: { title: 'Known', episode: 3, source: 'fallback' },
|
||||
mediaGuessPromise: null,
|
||||
lastDurationProbeAtMs: 11,
|
||||
});
|
||||
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaDurationSec, 90);
|
||||
|
||||
composed.resetAnilistMediaGuessState();
|
||||
assert.equal(composed.getAnilistMediaGuessRuntimeState().mediaGuess, null);
|
||||
|
||||
mediaKeyState = 'media-key';
|
||||
mediaDurationSecState = null;
|
||||
const probedDuration = await composed.maybeProbeAnilistDuration('media-key');
|
||||
assert.equal(probedDuration, 120);
|
||||
assert.equal(requestMpvDurationCalls, 1);
|
||||
|
||||
mediaGuessState = null;
|
||||
await composed.ensureAnilistMediaGuess('media-key');
|
||||
assert.equal(guessAnilistMediaInfoCalls, 1);
|
||||
|
||||
const retryResult = await composed.processNextAnilistRetryUpdate();
|
||||
assert.deepEqual(retryResult, { ok: true, message: 'ok' });
|
||||
assert.equal(retryUpdateCalls, 1);
|
||||
|
||||
await composed.maybeRunAnilistPostWatchUpdate();
|
||||
assert.equal(maybeRunUpdateCalls, 1);
|
||||
});
|
||||
129
src/main/runtime/composers/anilist-tracking-composer.ts
Normal file
129
src/main/runtime/composers/anilist-tracking-composer.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
createBuildEnsureAnilistMediaGuessMainDepsHandler,
|
||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler,
|
||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler,
|
||||
createBuildMaybeProbeAnilistDurationMainDepsHandler,
|
||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler,
|
||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler,
|
||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler,
|
||||
createBuildResetAnilistMediaGuessStateMainDepsHandler,
|
||||
createBuildResetAnilistMediaTrackingMainDepsHandler,
|
||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler,
|
||||
createEnsureAnilistMediaGuessHandler,
|
||||
createGetAnilistMediaGuessRuntimeStateHandler,
|
||||
createGetCurrentAnilistMediaKeyHandler,
|
||||
createMaybeProbeAnilistDurationHandler,
|
||||
createMaybeRunAnilistPostWatchUpdateHandler,
|
||||
createProcessNextAnilistRetryUpdateHandler,
|
||||
createRefreshAnilistClientSecretStateHandler,
|
||||
createResetAnilistMediaGuessStateHandler,
|
||||
createResetAnilistMediaTrackingHandler,
|
||||
createSetAnilistMediaGuessRuntimeStateHandler,
|
||||
} from '../domains/anilist';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
export type AnilistTrackingComposerOptions = ComposerInputs<{
|
||||
refreshClientSecretMainDeps: Parameters<
|
||||
typeof createBuildRefreshAnilistClientSecretStateMainDepsHandler
|
||||
>[0];
|
||||
getCurrentMediaKeyMainDeps: Parameters<
|
||||
typeof createBuildGetCurrentAnilistMediaKeyMainDepsHandler
|
||||
>[0];
|
||||
resetMediaTrackingMainDeps: Parameters<
|
||||
typeof createBuildResetAnilistMediaTrackingMainDepsHandler
|
||||
>[0];
|
||||
getMediaGuessRuntimeStateMainDeps: Parameters<
|
||||
typeof createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler
|
||||
>[0];
|
||||
setMediaGuessRuntimeStateMainDeps: Parameters<
|
||||
typeof createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler
|
||||
>[0];
|
||||
resetMediaGuessStateMainDeps: Parameters<
|
||||
typeof createBuildResetAnilistMediaGuessStateMainDepsHandler
|
||||
>[0];
|
||||
maybeProbeDurationMainDeps: Parameters<
|
||||
typeof createBuildMaybeProbeAnilistDurationMainDepsHandler
|
||||
>[0];
|
||||
ensureMediaGuessMainDeps: Parameters<typeof createBuildEnsureAnilistMediaGuessMainDepsHandler>[0];
|
||||
processNextRetryUpdateMainDeps: Parameters<
|
||||
typeof createBuildProcessNextAnilistRetryUpdateMainDepsHandler
|
||||
>[0];
|
||||
maybeRunPostWatchUpdateMainDeps: Parameters<
|
||||
typeof createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler
|
||||
>[0];
|
||||
}>;
|
||||
|
||||
export type AnilistTrackingComposerResult = ComposerOutputs<{
|
||||
refreshAnilistClientSecretState: ReturnType<typeof createRefreshAnilistClientSecretStateHandler>;
|
||||
getCurrentAnilistMediaKey: ReturnType<typeof createGetCurrentAnilistMediaKeyHandler>;
|
||||
resetAnilistMediaTracking: ReturnType<typeof createResetAnilistMediaTrackingHandler>;
|
||||
getAnilistMediaGuessRuntimeState: ReturnType<
|
||||
typeof createGetAnilistMediaGuessRuntimeStateHandler
|
||||
>;
|
||||
setAnilistMediaGuessRuntimeState: ReturnType<
|
||||
typeof createSetAnilistMediaGuessRuntimeStateHandler
|
||||
>;
|
||||
resetAnilistMediaGuessState: ReturnType<typeof createResetAnilistMediaGuessStateHandler>;
|
||||
maybeProbeAnilistDuration: ReturnType<typeof createMaybeProbeAnilistDurationHandler>;
|
||||
ensureAnilistMediaGuess: ReturnType<typeof createEnsureAnilistMediaGuessHandler>;
|
||||
processNextAnilistRetryUpdate: ReturnType<typeof createProcessNextAnilistRetryUpdateHandler>;
|
||||
maybeRunAnilistPostWatchUpdate: ReturnType<typeof createMaybeRunAnilistPostWatchUpdateHandler>;
|
||||
}>;
|
||||
|
||||
export function composeAnilistTrackingHandlers(
|
||||
options: AnilistTrackingComposerOptions,
|
||||
): AnilistTrackingComposerResult {
|
||||
const refreshAnilistClientSecretState = createRefreshAnilistClientSecretStateHandler(
|
||||
createBuildRefreshAnilistClientSecretStateMainDepsHandler(
|
||||
options.refreshClientSecretMainDeps,
|
||||
)(),
|
||||
);
|
||||
const getCurrentAnilistMediaKey = createGetCurrentAnilistMediaKeyHandler(
|
||||
createBuildGetCurrentAnilistMediaKeyMainDepsHandler(options.getCurrentMediaKeyMainDeps)(),
|
||||
);
|
||||
const resetAnilistMediaTracking = createResetAnilistMediaTrackingHandler(
|
||||
createBuildResetAnilistMediaTrackingMainDepsHandler(options.resetMediaTrackingMainDeps)(),
|
||||
);
|
||||
const getAnilistMediaGuessRuntimeState = createGetAnilistMediaGuessRuntimeStateHandler(
|
||||
createBuildGetAnilistMediaGuessRuntimeStateMainDepsHandler(
|
||||
options.getMediaGuessRuntimeStateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const setAnilistMediaGuessRuntimeState = createSetAnilistMediaGuessRuntimeStateHandler(
|
||||
createBuildSetAnilistMediaGuessRuntimeStateMainDepsHandler(
|
||||
options.setMediaGuessRuntimeStateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const resetAnilistMediaGuessState = createResetAnilistMediaGuessStateHandler(
|
||||
createBuildResetAnilistMediaGuessStateMainDepsHandler(options.resetMediaGuessStateMainDeps)(),
|
||||
);
|
||||
const maybeProbeAnilistDuration = createMaybeProbeAnilistDurationHandler(
|
||||
createBuildMaybeProbeAnilistDurationMainDepsHandler(options.maybeProbeDurationMainDeps)(),
|
||||
);
|
||||
const ensureAnilistMediaGuess = createEnsureAnilistMediaGuessHandler(
|
||||
createBuildEnsureAnilistMediaGuessMainDepsHandler(options.ensureMediaGuessMainDeps)(),
|
||||
);
|
||||
const processNextAnilistRetryUpdate = createProcessNextAnilistRetryUpdateHandler(
|
||||
createBuildProcessNextAnilistRetryUpdateMainDepsHandler(
|
||||
options.processNextRetryUpdateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const maybeRunAnilistPostWatchUpdate = createMaybeRunAnilistPostWatchUpdateHandler(
|
||||
createBuildMaybeRunAnilistPostWatchUpdateMainDepsHandler(
|
||||
options.maybeRunPostWatchUpdateMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
return {
|
||||
refreshAnilistClientSecretState,
|
||||
getCurrentAnilistMediaKey,
|
||||
resetAnilistMediaTracking,
|
||||
getAnilistMediaGuessRuntimeState,
|
||||
setAnilistMediaGuessRuntimeState,
|
||||
resetAnilistMediaGuessState,
|
||||
maybeProbeAnilistDuration,
|
||||
ensureAnilistMediaGuess,
|
||||
processNextAnilistRetryUpdate,
|
||||
maybeRunAnilistPostWatchUpdate,
|
||||
};
|
||||
}
|
||||
75
src/main/runtime/composers/app-ready-composer.test.ts
Normal file
75
src/main/runtime/composers/app-ready-composer.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeAppReadyRuntime } from './app-ready-composer';
|
||||
|
||||
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
|
||||
const composed = composeAppReadyRuntime({
|
||||
reloadConfigMainDeps: {
|
||||
reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
|
||||
logInfo: () => {},
|
||||
logWarning: () => {},
|
||||
showDesktopNotification: () => {},
|
||||
startConfigHotReload: () => {},
|
||||
refreshAnilistClientSecretState: async () => {},
|
||||
failHandlers: {
|
||||
logError: () => {},
|
||||
showErrorBox: () => {},
|
||||
quit: () => {},
|
||||
},
|
||||
},
|
||||
criticalConfigErrorMainDeps: {
|
||||
getConfigPath: () => '/tmp/config.jsonc',
|
||||
failHandlers: {
|
||||
logError: () => {},
|
||||
showErrorBox: () => {},
|
||||
quit: () => {},
|
||||
},
|
||||
},
|
||||
appReadyRuntimeMainDeps: {
|
||||
loadSubtitlePosition: () => {},
|
||||
resolveKeybindings: () => {},
|
||||
createMpvClient: () => {},
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getConfigWarnings: () => [],
|
||||
logConfigWarning: () => {},
|
||||
setLogLevel: () => {},
|
||||
initRuntimeOptionsManager: () => {},
|
||||
setSecondarySubMode: () => {},
|
||||
defaultSecondarySubMode: 'hover',
|
||||
defaultWebsocketPort: 5174,
|
||||
hasMpvWebsocketPlugin: () => false,
|
||||
startSubtitleWebsocket: () => {},
|
||||
log: () => {},
|
||||
createMecabTokenizerAndCheck: async () => {},
|
||||
createSubtitleTimingTracker: () => {},
|
||||
loadYomitanExtension: async () => {},
|
||||
startJellyfinRemoteSession: async () => {},
|
||||
prewarmSubtitleDictionaries: async () => {},
|
||||
startBackgroundWarmups: () => {},
|
||||
texthookerOnlyMode: false,
|
||||
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
|
||||
initializeOverlayRuntime: () => {},
|
||||
handleInitialArgs: () => {},
|
||||
logDebug: () => {},
|
||||
now: () => Date.now(),
|
||||
},
|
||||
immersionTrackerStartupMainDeps: {
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getConfiguredDbPath: () => '/tmp/immersion.sqlite',
|
||||
createTrackerService: () =>
|
||||
({
|
||||
startSession: () => {},
|
||||
}) as never,
|
||||
setTracker: () => {},
|
||||
getMpvClient: () => null,
|
||||
seedTrackerFromCurrentMedia: () => {},
|
||||
logInfo: () => {},
|
||||
logDebug: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.reloadConfig, 'function');
|
||||
assert.equal(typeof composed.criticalConfigError, 'function');
|
||||
assert.equal(typeof composed.appReadyRuntimeRunner, 'function');
|
||||
});
|
||||
59
src/main/runtime/composers/app-ready-composer.ts
Normal file
59
src/main/runtime/composers/app-ready-composer.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createAppReadyRuntimeRunner } from '../../app-lifecycle';
|
||||
import { createBuildAppReadyRuntimeMainDepsHandler } from '../app-ready-main-deps';
|
||||
import {
|
||||
createBuildCriticalConfigErrorMainDepsHandler,
|
||||
createBuildReloadConfigMainDepsHandler,
|
||||
} from '../startup-config-main-deps';
|
||||
import { createCriticalConfigErrorHandler, createReloadConfigHandler } from '../startup-config';
|
||||
import { createBuildImmersionTrackerStartupMainDepsHandler } from '../immersion-startup-main-deps';
|
||||
import { createImmersionTrackerStartupHandler } from '../immersion-startup';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type ReloadConfigMainDeps = Parameters<typeof createBuildReloadConfigMainDepsHandler>[0];
|
||||
type CriticalConfigErrorMainDeps = Parameters<
|
||||
typeof createBuildCriticalConfigErrorMainDepsHandler
|
||||
>[0];
|
||||
type AppReadyRuntimeMainDeps = Parameters<typeof createBuildAppReadyRuntimeMainDepsHandler>[0];
|
||||
|
||||
export type AppReadyComposerOptions = ComposerInputs<{
|
||||
reloadConfigMainDeps: ReloadConfigMainDeps;
|
||||
criticalConfigErrorMainDeps: CriticalConfigErrorMainDeps;
|
||||
appReadyRuntimeMainDeps: Omit<AppReadyRuntimeMainDeps, 'reloadConfig' | 'onCriticalConfigErrors'>;
|
||||
immersionTrackerStartupMainDeps: Parameters<
|
||||
typeof createBuildImmersionTrackerStartupMainDepsHandler
|
||||
>[0];
|
||||
}>;
|
||||
|
||||
export type AppReadyComposerResult = ComposerOutputs<{
|
||||
reloadConfig: ReturnType<typeof createReloadConfigHandler>;
|
||||
criticalConfigError: ReturnType<typeof createCriticalConfigErrorHandler>;
|
||||
appReadyRuntimeRunner: ReturnType<typeof createAppReadyRuntimeRunner>;
|
||||
}>;
|
||||
|
||||
export function composeAppReadyRuntime(options: AppReadyComposerOptions): AppReadyComposerResult {
|
||||
const reloadConfig = createReloadConfigHandler(
|
||||
createBuildReloadConfigMainDepsHandler(options.reloadConfigMainDeps)(),
|
||||
);
|
||||
const criticalConfigError = createCriticalConfigErrorHandler(
|
||||
createBuildCriticalConfigErrorMainDepsHandler(options.criticalConfigErrorMainDeps)(),
|
||||
);
|
||||
|
||||
const appReadyRuntimeRunner = createAppReadyRuntimeRunner(
|
||||
createBuildAppReadyRuntimeMainDepsHandler({
|
||||
...options.appReadyRuntimeMainDeps,
|
||||
reloadConfig,
|
||||
createImmersionTracker: createImmersionTrackerStartupHandler(
|
||||
createBuildImmersionTrackerStartupMainDepsHandler(
|
||||
options.immersionTrackerStartupMainDeps,
|
||||
)(),
|
||||
),
|
||||
onCriticalConfigErrors: criticalConfigError,
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
reloadConfig,
|
||||
criticalConfigError,
|
||||
appReadyRuntimeRunner,
|
||||
};
|
||||
}
|
||||
95
src/main/runtime/composers/composer-contracts.type-test.ts
Normal file
95
src/main/runtime/composers/composer-contracts.type-test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { ComposerInputs } from './contracts';
|
||||
import type { IpcRuntimeComposerOptions } from './ipc-runtime-composer';
|
||||
import type { JellyfinRemoteComposerOptions } from './jellyfin-remote-composer';
|
||||
import type { MpvRuntimeComposerOptions } from './mpv-runtime-composer';
|
||||
import type { AnilistSetupComposerOptions } from './anilist-setup-composer';
|
||||
|
||||
type Assert<T extends true> = T;
|
||||
type IsAssignable<From, To> = [From] extends [To] ? true : false;
|
||||
|
||||
type FakeMpvClient = {
|
||||
on: (...args: unknown[]) => unknown;
|
||||
connect: () => void;
|
||||
};
|
||||
|
||||
type FakeTokenizerDeps = { isKnownWord: (text: string) => boolean };
|
||||
type FakeTokenizedSubtitle = { text: string };
|
||||
|
||||
type RequiredAnilistSetupInputKeys = keyof ComposerInputs<AnilistSetupComposerOptions>;
|
||||
type RequiredJellyfinInputKeys = keyof ComposerInputs<JellyfinRemoteComposerOptions>;
|
||||
type RequiredIpcInputKeys = keyof ComposerInputs<IpcRuntimeComposerOptions>;
|
||||
type RequiredMpvInputKeys = keyof ComposerInputs<
|
||||
MpvRuntimeComposerOptions<FakeMpvClient, FakeTokenizerDeps, FakeTokenizedSubtitle>
|
||||
>;
|
||||
|
||||
type _anilistHasNotifyDeps = Assert<IsAssignable<'notifyDeps', RequiredAnilistSetupInputKeys>>;
|
||||
type _jellyfinHasGetMpvClient = Assert<IsAssignable<'getMpvClient', RequiredJellyfinInputKeys>>;
|
||||
type _ipcHasRegistration = Assert<IsAssignable<'registration', RequiredIpcInputKeys>>;
|
||||
type _mpvHasTokenizer = Assert<IsAssignable<'tokenizer', RequiredMpvInputKeys>>;
|
||||
|
||||
// @ts-expect-error missing required notifyDeps should fail compile-time contract
|
||||
const anilistMissingRequired: AnilistSetupComposerOptions = {
|
||||
consumeTokenDeps: {} as AnilistSetupComposerOptions['consumeTokenDeps'],
|
||||
handleProtocolDeps: {} as AnilistSetupComposerOptions['handleProtocolDeps'],
|
||||
registerProtocolClientDeps: {} as AnilistSetupComposerOptions['registerProtocolClientDeps'],
|
||||
};
|
||||
|
||||
// @ts-expect-error missing required getMpvClient should fail compile-time contract
|
||||
const jellyfinMissingRequired: JellyfinRemoteComposerOptions = {
|
||||
getConfiguredSession: {} as JellyfinRemoteComposerOptions['getConfiguredSession'],
|
||||
getClientInfo: {} as JellyfinRemoteComposerOptions['getClientInfo'],
|
||||
getJellyfinConfig: {} as JellyfinRemoteComposerOptions['getJellyfinConfig'],
|
||||
playJellyfinItem: {} as JellyfinRemoteComposerOptions['playJellyfinItem'],
|
||||
logWarn: {} as JellyfinRemoteComposerOptions['logWarn'],
|
||||
sendMpvCommand: {} as JellyfinRemoteComposerOptions['sendMpvCommand'],
|
||||
jellyfinTicksToSeconds: {} as JellyfinRemoteComposerOptions['jellyfinTicksToSeconds'],
|
||||
getActivePlayback: {} as JellyfinRemoteComposerOptions['getActivePlayback'],
|
||||
clearActivePlayback: {} as JellyfinRemoteComposerOptions['clearActivePlayback'],
|
||||
getSession: {} as JellyfinRemoteComposerOptions['getSession'],
|
||||
getNow: {} as JellyfinRemoteComposerOptions['getNow'],
|
||||
getLastProgressAtMs: {} as JellyfinRemoteComposerOptions['getLastProgressAtMs'],
|
||||
setLastProgressAtMs: {} as JellyfinRemoteComposerOptions['setLastProgressAtMs'],
|
||||
progressIntervalMs: 3000,
|
||||
ticksPerSecond: 10_000_000,
|
||||
logDebug: {} as JellyfinRemoteComposerOptions['logDebug'],
|
||||
};
|
||||
|
||||
// @ts-expect-error missing required registration should fail compile-time contract
|
||||
const ipcMissingRequired: IpcRuntimeComposerOptions = {
|
||||
mpvCommandMainDeps: {} as IpcRuntimeComposerOptions['mpvCommandMainDeps'],
|
||||
handleMpvCommandFromIpcRuntime: {} as IpcRuntimeComposerOptions['handleMpvCommandFromIpcRuntime'],
|
||||
runSubsyncManualFromIpc: {} as IpcRuntimeComposerOptions['runSubsyncManualFromIpc'],
|
||||
};
|
||||
|
||||
// @ts-expect-error missing required tokenizer should fail compile-time contract
|
||||
const mpvMissingRequired: MpvRuntimeComposerOptions<
|
||||
FakeMpvClient,
|
||||
FakeTokenizerDeps,
|
||||
FakeTokenizedSubtitle
|
||||
> = {
|
||||
bindMpvMainEventHandlersMainDeps: {} as MpvRuntimeComposerOptions<
|
||||
FakeMpvClient,
|
||||
FakeTokenizerDeps,
|
||||
FakeTokenizedSubtitle
|
||||
>['bindMpvMainEventHandlersMainDeps'],
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {} as MpvRuntimeComposerOptions<
|
||||
FakeMpvClient,
|
||||
FakeTokenizerDeps,
|
||||
FakeTokenizedSubtitle
|
||||
>['mpvClientRuntimeServiceFactoryMainDeps'],
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {} as MpvRuntimeComposerOptions<
|
||||
FakeMpvClient,
|
||||
FakeTokenizerDeps,
|
||||
FakeTokenizedSubtitle
|
||||
>['updateMpvSubtitleRenderMetricsMainDeps'],
|
||||
warmups: {} as MpvRuntimeComposerOptions<
|
||||
FakeMpvClient,
|
||||
FakeTokenizerDeps,
|
||||
FakeTokenizedSubtitle
|
||||
>['warmups'],
|
||||
};
|
||||
|
||||
void anilistMissingRequired;
|
||||
void jellyfinMissingRequired;
|
||||
void ipcMissingRequired;
|
||||
void mpvMissingRequired;
|
||||
13
src/main/runtime/composers/contracts.ts
Normal file
13
src/main/runtime/composers/contracts.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
type ComposerShape = Record<string, unknown>;
|
||||
|
||||
export type ComposerInputs<T extends ComposerShape> = Readonly<Required<T>>;
|
||||
|
||||
export type ComposerOutputs<T extends ComposerShape> = Readonly<T>;
|
||||
|
||||
export type BuiltMainDeps<TFactory> = TFactory extends (
|
||||
...args: infer _TFactoryArgs
|
||||
) => infer TBuilder
|
||||
? TBuilder extends (...args: infer _TBuilderArgs) => infer TDeps
|
||||
? TDeps
|
||||
: never
|
||||
: never;
|
||||
10
src/main/runtime/composers/index.ts
Normal file
10
src/main/runtime/composers/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './anilist-setup-composer';
|
||||
export * from './anilist-tracking-composer';
|
||||
export * from './app-ready-composer';
|
||||
export * from './contracts';
|
||||
export * from './ipc-runtime-composer';
|
||||
export * from './jellyfin-remote-composer';
|
||||
export * from './jellyfin-runtime-composer';
|
||||
export * from './mpv-runtime-composer';
|
||||
export * from './shortcuts-runtime-composer';
|
||||
export * from './startup-lifecycle-composer';
|
||||
109
src/main/runtime/composers/ipc-runtime-composer.test.ts
Normal file
109
src/main/runtime/composers/ipc-runtime-composer.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeIpcRuntimeHandlers } from './ipc-runtime-composer';
|
||||
|
||||
test('composeIpcRuntimeHandlers returns callable IPC handlers and registration bridge', async () => {
|
||||
let registered = false;
|
||||
let receivedSourceTrackId: number | null | undefined;
|
||||
|
||||
const composed = composeIpcRuntimeHandlers({
|
||||
mpvCommandMainDeps: {
|
||||
triggerSubsyncFromConfig: async () => {},
|
||||
openRuntimeOptionsPalette: () => {},
|
||||
cycleRuntimeOption: () => ({ ok: true }),
|
||||
showMpvOsd: () => {},
|
||||
replayCurrentSubtitle: () => {},
|
||||
playNextSubtitle: () => {},
|
||||
sendMpvCommand: () => {},
|
||||
isMpvConnected: () => false,
|
||||
hasRuntimeOptionsManager: () => true,
|
||||
},
|
||||
handleMpvCommandFromIpcRuntime: () => {},
|
||||
runSubsyncManualFromIpc: async (request) => {
|
||||
receivedSourceTrackId = request.sourceTrackId;
|
||||
return {
|
||||
ok: true,
|
||||
message: 'ok',
|
||||
};
|
||||
},
|
||||
registration: {
|
||||
runtimeOptions: {
|
||||
getRuntimeOptionsManager: () => null,
|
||||
showMpvOsd: () => {},
|
||||
},
|
||||
mainDeps: {
|
||||
getInvisibleWindow: () => null,
|
||||
getMainWindow: () => null,
|
||||
getVisibleOverlayVisibility: () => false,
|
||||
getInvisibleOverlayVisibility: () => false,
|
||||
focusMainWindow: () => {},
|
||||
onOverlayModalClosed: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
quitApp: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
tokenizeCurrentSubtitle: async () => null,
|
||||
getCurrentSubtitleRaw: () => '',
|
||||
getCurrentSubtitleAss: () => '',
|
||||
getMpvSubtitleRenderMetrics: () => ({}) as never,
|
||||
getSubtitlePosition: () => ({}) as never,
|
||||
getSubtitleStyle: () => ({}) as never,
|
||||
saveSubtitlePosition: () => {},
|
||||
getMecabTokenizer: () => null,
|
||||
getKeybindings: () => [],
|
||||
getConfiguredShortcuts: () => ({}) as never,
|
||||
getSecondarySubMode: () => 'hover' as never,
|
||||
getMpvClient: () => null,
|
||||
getAnkiConnectStatus: () => false,
|
||||
getRuntimeOptions: () => [],
|
||||
reportOverlayContentBounds: () => {},
|
||||
reportHoveredSubtitleToken: () => {},
|
||||
getAnilistStatus: () => ({}) as never,
|
||||
clearAnilistToken: () => {},
|
||||
openAnilistSetup: () => {},
|
||||
getAnilistQueueStatus: () => ({}) as never,
|
||||
retryAnilistQueueNow: async () => ({ ok: true, message: 'ok' }),
|
||||
appendClipboardVideoToQueue: () => ({ ok: true, message: 'ok' }),
|
||||
},
|
||||
ankiJimakuDeps: {
|
||||
patchAnkiConnectEnabled: () => {},
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
getRuntimeOptionsManager: () => null,
|
||||
getSubtitleTimingTracker: () => null,
|
||||
getMpvClient: () => null,
|
||||
getAnkiIntegration: () => null,
|
||||
setAnkiIntegration: () => {},
|
||||
getKnownWordCacheStatePath: () => '',
|
||||
showDesktopNotification: () => {},
|
||||
createFieldGroupingCallback: () => (() => {}) as never,
|
||||
broadcastRuntimeOptionsChanged: () => {},
|
||||
getFieldGroupingResolver: () => null,
|
||||
setFieldGroupingResolver: () => {},
|
||||
parseMediaInfo: () => ({}) as never,
|
||||
getCurrentMediaPath: () => null,
|
||||
jimakuFetchJson: async () => ({ data: null }) as never,
|
||||
getJimakuMaxEntryResults: () => 0,
|
||||
getJimakuLanguagePreference: () => 'ja' as never,
|
||||
resolveJimakuApiKey: async () => null,
|
||||
isRemoteMediaPath: () => false,
|
||||
downloadToFile: async () => ({ ok: true, path: '/tmp/file' }),
|
||||
},
|
||||
registerIpcRuntimeServices: () => {
|
||||
registered = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.handleMpvCommandFromIpc, 'function');
|
||||
assert.equal(typeof composed.runSubsyncManualFromIpc, 'function');
|
||||
assert.equal(typeof composed.registerIpcRuntimeHandlers, 'function');
|
||||
|
||||
const result = await composed.runSubsyncManualFromIpc({
|
||||
engine: 'alass',
|
||||
sourceTrackId: 7,
|
||||
});
|
||||
assert.deepEqual(result, { ok: true, message: 'ok' });
|
||||
assert.equal(receivedSourceTrackId, 7);
|
||||
|
||||
composed.registerIpcRuntimeHandlers();
|
||||
assert.equal(registered, true);
|
||||
});
|
||||
73
src/main/runtime/composers/ipc-runtime-composer.ts
Normal file
73
src/main/runtime/composers/ipc-runtime-composer.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { RegisterIpcRuntimeServicesParams } from '../../ipc-runtime';
|
||||
import type { SubsyncManualRunRequest, SubsyncResult } from '../../../types';
|
||||
import {
|
||||
createBuildMpvCommandFromIpcRuntimeMainDepsHandler,
|
||||
createIpcRuntimeHandlers,
|
||||
} from '../domains/ipc';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type MpvCommand = (string | number)[];
|
||||
|
||||
type IpcMainDeps = RegisterIpcRuntimeServicesParams['mainDeps'];
|
||||
type IpcMainDepsWithoutHandlers = Omit<IpcMainDeps, 'handleMpvCommand' | 'runSubsyncManual'>;
|
||||
type RunSubsyncManual = IpcMainDeps['runSubsyncManual'];
|
||||
|
||||
type IpcRuntimeDeps = Parameters<
|
||||
typeof createIpcRuntimeHandlers<SubsyncManualRunRequest, SubsyncResult>
|
||||
>[0];
|
||||
|
||||
export type IpcRuntimeComposerOptions = ComposerInputs<{
|
||||
mpvCommandMainDeps: Parameters<typeof createBuildMpvCommandFromIpcRuntimeMainDepsHandler>[0];
|
||||
handleMpvCommandFromIpcRuntime: IpcRuntimeDeps['handleMpvCommandFromIpcDeps']['handleMpvCommandFromIpcRuntime'];
|
||||
runSubsyncManualFromIpc: RunSubsyncManual;
|
||||
registration: {
|
||||
runtimeOptions: RegisterIpcRuntimeServicesParams['runtimeOptions'];
|
||||
mainDeps: IpcMainDepsWithoutHandlers;
|
||||
ankiJimakuDeps: RegisterIpcRuntimeServicesParams['ankiJimakuDeps'];
|
||||
registerIpcRuntimeServices: (params: RegisterIpcRuntimeServicesParams) => void;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type IpcRuntimeComposerResult = ComposerOutputs<{
|
||||
handleMpvCommandFromIpc: (command: MpvCommand) => void;
|
||||
runSubsyncManualFromIpc: RunSubsyncManual;
|
||||
registerIpcRuntimeHandlers: () => void;
|
||||
}>;
|
||||
|
||||
export function composeIpcRuntimeHandlers(
|
||||
options: IpcRuntimeComposerOptions,
|
||||
): IpcRuntimeComposerResult {
|
||||
const mpvCommandFromIpcRuntimeMainDeps = createBuildMpvCommandFromIpcRuntimeMainDepsHandler(
|
||||
options.mpvCommandMainDeps,
|
||||
)();
|
||||
const { handleMpvCommandFromIpc, runSubsyncManualFromIpc } = createIpcRuntimeHandlers<
|
||||
SubsyncManualRunRequest,
|
||||
SubsyncResult
|
||||
>({
|
||||
handleMpvCommandFromIpcDeps: {
|
||||
handleMpvCommandFromIpcRuntime: options.handleMpvCommandFromIpcRuntime,
|
||||
buildMpvCommandDeps: () => mpvCommandFromIpcRuntimeMainDeps,
|
||||
},
|
||||
runSubsyncManualFromIpcDeps: {
|
||||
runManualFromIpc: (request) => options.runSubsyncManualFromIpc(request),
|
||||
},
|
||||
});
|
||||
|
||||
const registerIpcRuntimeHandlers = (): void => {
|
||||
options.registration.registerIpcRuntimeServices({
|
||||
runtimeOptions: options.registration.runtimeOptions,
|
||||
mainDeps: {
|
||||
...options.registration.mainDeps,
|
||||
handleMpvCommand: (command) => handleMpvCommandFromIpc(command),
|
||||
runSubsyncManual: (request) => runSubsyncManualFromIpc(request),
|
||||
},
|
||||
ankiJimakuDeps: options.registration.ankiJimakuDeps,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
handleMpvCommandFromIpc: (command) => handleMpvCommandFromIpc(command),
|
||||
runSubsyncManualFromIpc: (request) => runSubsyncManualFromIpc(request),
|
||||
registerIpcRuntimeHandlers,
|
||||
};
|
||||
}
|
||||
34
src/main/runtime/composers/jellyfin-remote-composer.test.ts
Normal file
34
src/main/runtime/composers/jellyfin-remote-composer.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { composeJellyfinRemoteHandlers } from './jellyfin-remote-composer';
|
||||
|
||||
test('composeJellyfinRemoteHandlers returns callable jellyfin remote handlers', () => {
|
||||
let lastProgressAt = 0;
|
||||
const composed = composeJellyfinRemoteHandlers({
|
||||
getConfiguredSession: () => null,
|
||||
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: 'test', deviceId: 'dev' }) as never,
|
||||
getJellyfinConfig: () => ({ enabled: false }) as never,
|
||||
playJellyfinItem: async () => {},
|
||||
logWarn: () => {},
|
||||
getMpvClient: () => null,
|
||||
sendMpvCommand: () => {},
|
||||
jellyfinTicksToSeconds: () => 0,
|
||||
getActivePlayback: () => null,
|
||||
clearActivePlayback: () => {},
|
||||
getSession: () => null,
|
||||
getNow: () => 0,
|
||||
getLastProgressAtMs: () => lastProgressAt,
|
||||
setLastProgressAtMs: (next) => {
|
||||
lastProgressAt = next;
|
||||
},
|
||||
progressIntervalMs: 3000,
|
||||
ticksPerSecond: 10_000_000,
|
||||
logDebug: () => {},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.reportJellyfinRemoteProgress, 'function');
|
||||
assert.equal(typeof composed.reportJellyfinRemoteStopped, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemotePlay, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemotePlaystate, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemoteGeneralCommand, 'function');
|
||||
});
|
||||
137
src/main/runtime/composers/jellyfin-remote-composer.ts
Normal file
137
src/main/runtime/composers/jellyfin-remote-composer.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler,
|
||||
createBuildHandleJellyfinRemotePlayMainDepsHandler,
|
||||
createBuildHandleJellyfinRemotePlaystateMainDepsHandler,
|
||||
createBuildReportJellyfinRemoteProgressMainDepsHandler,
|
||||
createBuildReportJellyfinRemoteStoppedMainDepsHandler,
|
||||
createHandleJellyfinRemoteGeneralCommand,
|
||||
createHandleJellyfinRemotePlay,
|
||||
createHandleJellyfinRemotePlaystate,
|
||||
createReportJellyfinRemoteProgressHandler,
|
||||
createReportJellyfinRemoteStoppedHandler,
|
||||
} from '../domains/jellyfin';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type RemotePlayPayload = Parameters<ReturnType<typeof createHandleJellyfinRemotePlay>>[0];
|
||||
type RemotePlaystatePayload = Parameters<ReturnType<typeof createHandleJellyfinRemotePlaystate>>[0];
|
||||
type RemoteGeneralPayload = Parameters<
|
||||
ReturnType<typeof createHandleJellyfinRemoteGeneralCommand>
|
||||
>[0];
|
||||
type JellyfinRemotePlayMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlayMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemotePlaystateMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemoteGeneralMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler
|
||||
>[0];
|
||||
type JellyfinRemoteProgressMainDeps = Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0];
|
||||
|
||||
export type JellyfinRemoteComposerOptions = ComposerInputs<{
|
||||
getConfiguredSession: JellyfinRemotePlayMainDeps['getConfiguredSession'];
|
||||
getClientInfo: JellyfinRemotePlayMainDeps['getClientInfo'];
|
||||
getJellyfinConfig: JellyfinRemotePlayMainDeps['getJellyfinConfig'];
|
||||
playJellyfinItem: JellyfinRemotePlayMainDeps['playJellyfinItem'];
|
||||
logWarn: JellyfinRemotePlayMainDeps['logWarn'];
|
||||
getMpvClient: JellyfinRemoteProgressMainDeps['getMpvClient'];
|
||||
sendMpvCommand: JellyfinRemotePlaystateMainDeps['sendMpvCommand'];
|
||||
jellyfinTicksToSeconds: Parameters<
|
||||
typeof createBuildHandleJellyfinRemotePlaystateMainDepsHandler
|
||||
>[0]['jellyfinTicksToSeconds'];
|
||||
getActivePlayback: JellyfinRemoteGeneralMainDeps['getActivePlayback'];
|
||||
clearActivePlayback: JellyfinRemoteProgressMainDeps['clearActivePlayback'];
|
||||
getSession: JellyfinRemoteProgressMainDeps['getSession'];
|
||||
getNow: JellyfinRemoteProgressMainDeps['getNow'];
|
||||
getLastProgressAtMs: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['getLastProgressAtMs'];
|
||||
setLastProgressAtMs: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['setLastProgressAtMs'];
|
||||
progressIntervalMs: number;
|
||||
ticksPerSecond: number;
|
||||
logDebug: Parameters<
|
||||
typeof createBuildReportJellyfinRemoteProgressMainDepsHandler
|
||||
>[0]['logDebug'];
|
||||
}>;
|
||||
|
||||
export type JellyfinRemoteComposerResult = ComposerOutputs<{
|
||||
reportJellyfinRemoteProgress: ReturnType<typeof createReportJellyfinRemoteProgressHandler>;
|
||||
reportJellyfinRemoteStopped: ReturnType<typeof createReportJellyfinRemoteStoppedHandler>;
|
||||
handleJellyfinRemotePlay: (payload: RemotePlayPayload) => Promise<void>;
|
||||
handleJellyfinRemotePlaystate: (payload: RemotePlaystatePayload) => Promise<void>;
|
||||
handleJellyfinRemoteGeneralCommand: (payload: RemoteGeneralPayload) => Promise<void>;
|
||||
}>;
|
||||
|
||||
export function composeJellyfinRemoteHandlers(
|
||||
options: JellyfinRemoteComposerOptions,
|
||||
): JellyfinRemoteComposerResult {
|
||||
const buildReportJellyfinRemoteProgressMainDepsHandler =
|
||||
createBuildReportJellyfinRemoteProgressMainDepsHandler({
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getSession: options.getSession,
|
||||
getMpvClient: options.getMpvClient,
|
||||
getNow: options.getNow,
|
||||
getLastProgressAtMs: options.getLastProgressAtMs,
|
||||
setLastProgressAtMs: options.setLastProgressAtMs,
|
||||
progressIntervalMs: options.progressIntervalMs,
|
||||
ticksPerSecond: options.ticksPerSecond,
|
||||
logDebug: options.logDebug,
|
||||
});
|
||||
const buildReportJellyfinRemoteStoppedMainDepsHandler =
|
||||
createBuildReportJellyfinRemoteStoppedMainDepsHandler({
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
clearActivePlayback: options.clearActivePlayback,
|
||||
getSession: options.getSession,
|
||||
logDebug: options.logDebug,
|
||||
});
|
||||
const reportJellyfinRemoteProgress = createReportJellyfinRemoteProgressHandler(
|
||||
buildReportJellyfinRemoteProgressMainDepsHandler(),
|
||||
);
|
||||
const reportJellyfinRemoteStopped = createReportJellyfinRemoteStoppedHandler(
|
||||
buildReportJellyfinRemoteStoppedMainDepsHandler(),
|
||||
);
|
||||
|
||||
const buildHandleJellyfinRemotePlayMainDepsHandler =
|
||||
createBuildHandleJellyfinRemotePlayMainDepsHandler({
|
||||
getConfiguredSession: options.getConfiguredSession,
|
||||
getClientInfo: options.getClientInfo,
|
||||
getJellyfinConfig: options.getJellyfinConfig,
|
||||
playJellyfinItem: options.playJellyfinItem,
|
||||
logWarn: options.logWarn,
|
||||
});
|
||||
const buildHandleJellyfinRemotePlaystateMainDepsHandler =
|
||||
createBuildHandleJellyfinRemotePlaystateMainDepsHandler({
|
||||
getMpvClient: options.getMpvClient,
|
||||
sendMpvCommand: options.sendMpvCommand,
|
||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||
reportJellyfinRemoteStopped: () => reportJellyfinRemoteStopped(),
|
||||
jellyfinTicksToSeconds: options.jellyfinTicksToSeconds,
|
||||
});
|
||||
const buildHandleJellyfinRemoteGeneralCommandMainDepsHandler =
|
||||
createBuildHandleJellyfinRemoteGeneralCommandMainDepsHandler({
|
||||
getMpvClient: options.getMpvClient,
|
||||
sendMpvCommand: options.sendMpvCommand,
|
||||
getActivePlayback: options.getActivePlayback,
|
||||
reportJellyfinRemoteProgress: (force) => reportJellyfinRemoteProgress(force),
|
||||
logDebug: (message) => options.logDebug(message, undefined),
|
||||
});
|
||||
|
||||
return {
|
||||
reportJellyfinRemoteProgress,
|
||||
reportJellyfinRemoteStopped,
|
||||
handleJellyfinRemotePlay: createHandleJellyfinRemotePlay(
|
||||
buildHandleJellyfinRemotePlayMainDepsHandler(),
|
||||
),
|
||||
handleJellyfinRemotePlaystate: createHandleJellyfinRemotePlaystate(
|
||||
buildHandleJellyfinRemotePlaystateMainDepsHandler(),
|
||||
),
|
||||
handleJellyfinRemoteGeneralCommand: createHandleJellyfinRemoteGeneralCommand(
|
||||
buildHandleJellyfinRemoteGeneralCommandMainDepsHandler(),
|
||||
),
|
||||
};
|
||||
}
|
||||
192
src/main/runtime/composers/jellyfin-runtime-composer.test.ts
Normal file
192
src/main/runtime/composers/jellyfin-runtime-composer.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeJellyfinRuntimeHandlers } from './jellyfin-runtime-composer';
|
||||
|
||||
test('composeJellyfinRuntimeHandlers returns callable jellyfin runtime handlers', () => {
|
||||
let activePlayback: unknown = null;
|
||||
let lastProgressAtMs = 0;
|
||||
const composed = composeJellyfinRuntimeHandlers({
|
||||
getResolvedJellyfinConfigMainDeps: {
|
||||
getResolvedConfig: () => ({ jellyfin: { enabled: false, serverUrl: '' } }) as never,
|
||||
loadStoredSession: () => null,
|
||||
getEnv: () => undefined,
|
||||
},
|
||||
getJellyfinClientInfoMainDeps: {
|
||||
getResolvedJellyfinConfig: () => ({}) as never,
|
||||
getDefaultJellyfinConfig: () => ({
|
||||
clientName: 'SubMiner',
|
||||
clientVersion: 'test',
|
||||
deviceId: 'dev',
|
||||
}),
|
||||
},
|
||||
waitForMpvConnectedMainDeps: {
|
||||
getMpvClient: () => null,
|
||||
now: () => Date.now(),
|
||||
sleep: async () => {},
|
||||
},
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: {
|
||||
getSocketPath: () => '/tmp/test-mpv.sock',
|
||||
platform: 'linux',
|
||||
execPath: process.execPath,
|
||||
defaultMpvLogPath: '/tmp/test-mpv.log',
|
||||
defaultMpvArgs: [],
|
||||
removeSocketPath: () => {},
|
||||
spawnMpv: () => ({ unref: () => {} }) as never,
|
||||
logWarn: () => {},
|
||||
logInfo: () => {},
|
||||
},
|
||||
ensureMpvConnectedForJellyfinPlaybackMainDeps: {
|
||||
getMpvClient: () => null,
|
||||
setMpvClient: () => {},
|
||||
createMpvClient: () => ({}) as never,
|
||||
getAutoLaunchInFlight: () => null,
|
||||
setAutoLaunchInFlight: () => {},
|
||||
connectTimeoutMs: 10,
|
||||
autoLaunchTimeoutMs: 10,
|
||||
},
|
||||
preloadJellyfinExternalSubtitlesMainDeps: {
|
||||
listJellyfinSubtitleTracks: async () => [],
|
||||
getMpvClient: () => null,
|
||||
sendMpvCommand: () => {},
|
||||
wait: async () => {},
|
||||
logDebug: () => {},
|
||||
},
|
||||
playJellyfinItemInMpvMainDeps: {
|
||||
getMpvClient: () => null,
|
||||
resolvePlaybackPlan: async () => ({
|
||||
mode: 'direct',
|
||||
url: 'https://example.test/video.m3u8',
|
||||
title: 'Episode 1',
|
||||
startTimeTicks: 0,
|
||||
audioStreamIndex: null,
|
||||
subtitleStreamIndex: null,
|
||||
}),
|
||||
applyJellyfinMpvDefaults: () => {},
|
||||
sendMpvCommand: () => {},
|
||||
armQuitOnDisconnect: () => {},
|
||||
schedule: () => undefined,
|
||||
convertTicksToSeconds: () => 0,
|
||||
setActivePlayback: (value) => {
|
||||
activePlayback = value;
|
||||
},
|
||||
setLastProgressAtMs: (value) => {
|
||||
lastProgressAtMs = value;
|
||||
},
|
||||
reportPlaying: () => {},
|
||||
showMpvOsd: () => {},
|
||||
},
|
||||
remoteComposerOptions: {
|
||||
getConfiguredSession: () => null,
|
||||
logWarn: () => {},
|
||||
getMpvClient: () => null,
|
||||
sendMpvCommand: () => {},
|
||||
jellyfinTicksToSeconds: () => 0,
|
||||
getActivePlayback: () => activePlayback as never,
|
||||
clearActivePlayback: () => {
|
||||
activePlayback = null;
|
||||
},
|
||||
getSession: () => null,
|
||||
getNow: () => Date.now(),
|
||||
getLastProgressAtMs: () => lastProgressAtMs,
|
||||
setLastProgressAtMs: (value) => {
|
||||
lastProgressAtMs = value;
|
||||
},
|
||||
progressIntervalMs: 3000,
|
||||
ticksPerSecond: 10_000_000,
|
||||
logDebug: () => {},
|
||||
},
|
||||
handleJellyfinAuthCommandsMainDeps: {
|
||||
patchRawConfig: () => {},
|
||||
authenticateWithPassword: async () => ({
|
||||
serverUrl: 'https://example.test',
|
||||
username: 'user',
|
||||
accessToken: 'token',
|
||||
userId: 'id',
|
||||
}),
|
||||
saveStoredSession: () => {},
|
||||
clearStoredSession: () => {},
|
||||
logInfo: () => {},
|
||||
},
|
||||
handleJellyfinListCommandsMainDeps: {
|
||||
listJellyfinLibraries: async () => [],
|
||||
listJellyfinItems: async () => [],
|
||||
listJellyfinSubtitleTracks: async () => [],
|
||||
logInfo: () => {},
|
||||
},
|
||||
handleJellyfinPlayCommandMainDeps: {
|
||||
logWarn: () => {},
|
||||
},
|
||||
handleJellyfinRemoteAnnounceCommandMainDeps: {
|
||||
getRemoteSession: () => null,
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
startJellyfinRemoteSessionMainDeps: {
|
||||
getCurrentSession: () => null,
|
||||
setCurrentSession: () => {},
|
||||
createRemoteSessionService: () =>
|
||||
({
|
||||
start: async () => {},
|
||||
}) as never,
|
||||
defaultDeviceId: 'dev',
|
||||
defaultClientName: 'SubMiner',
|
||||
defaultClientVersion: 'test',
|
||||
logInfo: () => {},
|
||||
logWarn: () => {},
|
||||
},
|
||||
stopJellyfinRemoteSessionMainDeps: {
|
||||
getCurrentSession: () => null,
|
||||
setCurrentSession: () => {},
|
||||
clearActivePlayback: () => {
|
||||
activePlayback = null;
|
||||
},
|
||||
},
|
||||
runJellyfinCommandMainDeps: {
|
||||
defaultServerUrl: 'https://example.test',
|
||||
},
|
||||
maybeFocusExistingJellyfinSetupWindowMainDeps: {
|
||||
getSetupWindow: () => null,
|
||||
},
|
||||
openJellyfinSetupWindowMainDeps: {
|
||||
createSetupWindow: () =>
|
||||
({
|
||||
focus: () => {},
|
||||
webContents: { on: () => {} },
|
||||
loadURL: () => {},
|
||||
on: () => {},
|
||||
isDestroyed: () => false,
|
||||
close: () => {},
|
||||
}) as never,
|
||||
buildSetupFormHtml: (defaultServer, defaultUser) =>
|
||||
`<html>${defaultServer}${defaultUser}</html>`,
|
||||
parseSubmissionUrl: () => null,
|
||||
authenticateWithPassword: async () => ({
|
||||
serverUrl: 'https://example.test',
|
||||
username: 'user',
|
||||
accessToken: 'token',
|
||||
userId: 'id',
|
||||
}),
|
||||
saveStoredSession: () => {},
|
||||
patchJellyfinConfig: () => {},
|
||||
logInfo: () => {},
|
||||
logError: () => {},
|
||||
showMpvOsd: () => {},
|
||||
clearSetupWindow: () => {},
|
||||
setSetupWindow: () => {},
|
||||
encodeURIComponent,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.getResolvedJellyfinConfig, 'function');
|
||||
assert.equal(typeof composed.getJellyfinClientInfo, 'function');
|
||||
assert.equal(typeof composed.reportJellyfinRemoteProgress, 'function');
|
||||
assert.equal(typeof composed.reportJellyfinRemoteStopped, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemotePlay, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemotePlaystate, 'function');
|
||||
assert.equal(typeof composed.handleJellyfinRemoteGeneralCommand, 'function');
|
||||
assert.equal(typeof composed.playJellyfinItemInMpv, 'function');
|
||||
assert.equal(typeof composed.startJellyfinRemoteSession, 'function');
|
||||
assert.equal(typeof composed.stopJellyfinRemoteSession, 'function');
|
||||
assert.equal(typeof composed.runJellyfinCommand, 'function');
|
||||
assert.equal(typeof composed.openJellyfinSetupWindow, 'function');
|
||||
});
|
||||
290
src/main/runtime/composers/jellyfin-runtime-composer.ts
Normal file
290
src/main/runtime/composers/jellyfin-runtime-composer.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import {
|
||||
buildJellyfinSetupFormHtml,
|
||||
createEnsureMpvConnectedForJellyfinPlaybackHandler,
|
||||
createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler,
|
||||
createBuildGetJellyfinClientInfoMainDepsHandler,
|
||||
createBuildGetResolvedJellyfinConfigMainDepsHandler,
|
||||
createBuildHandleJellyfinAuthCommandsMainDepsHandler,
|
||||
createBuildHandleJellyfinListCommandsMainDepsHandler,
|
||||
createBuildHandleJellyfinPlayCommandMainDepsHandler,
|
||||
createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler,
|
||||
createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler,
|
||||
createBuildOpenJellyfinSetupWindowMainDepsHandler,
|
||||
createBuildPlayJellyfinItemInMpvMainDepsHandler,
|
||||
createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler,
|
||||
createBuildRunJellyfinCommandMainDepsHandler,
|
||||
createBuildStartJellyfinRemoteSessionMainDepsHandler,
|
||||
createBuildStopJellyfinRemoteSessionMainDepsHandler,
|
||||
createBuildWaitForMpvConnectedMainDepsHandler,
|
||||
createGetJellyfinClientInfoHandler,
|
||||
createGetResolvedJellyfinConfigHandler,
|
||||
createHandleJellyfinAuthCommands,
|
||||
createHandleJellyfinListCommands,
|
||||
createHandleJellyfinPlayCommand,
|
||||
createHandleJellyfinRemoteAnnounceCommand,
|
||||
createLaunchMpvIdleForJellyfinPlaybackHandler,
|
||||
createOpenJellyfinSetupWindowHandler,
|
||||
createPlayJellyfinItemInMpvHandler,
|
||||
createPreloadJellyfinExternalSubtitlesHandler,
|
||||
createRunJellyfinCommandHandler,
|
||||
createStartJellyfinRemoteSessionHandler,
|
||||
createStopJellyfinRemoteSessionHandler,
|
||||
createWaitForMpvConnectedHandler,
|
||||
createMaybeFocusExistingJellyfinSetupWindowHandler,
|
||||
parseJellyfinSetupSubmissionUrl,
|
||||
} from '../domains/jellyfin';
|
||||
import {
|
||||
composeJellyfinRemoteHandlers,
|
||||
type JellyfinRemoteComposerOptions,
|
||||
} from './jellyfin-remote-composer';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type EnsureMpvConnectedMainDeps = Parameters<
|
||||
typeof createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler
|
||||
>[0];
|
||||
type PlayJellyfinItemMainDeps = Parameters<
|
||||
typeof createBuildPlayJellyfinItemInMpvMainDepsHandler
|
||||
>[0];
|
||||
type HandlePlayCommandMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinPlayCommandMainDepsHandler
|
||||
>[0];
|
||||
type HandleRemoteAnnounceMainDeps = Parameters<
|
||||
typeof createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler
|
||||
>[0];
|
||||
type StartRemoteSessionMainDeps = Parameters<
|
||||
typeof createBuildStartJellyfinRemoteSessionMainDepsHandler
|
||||
>[0];
|
||||
type RunJellyfinCommandMainDeps = Parameters<
|
||||
typeof createBuildRunJellyfinCommandMainDepsHandler
|
||||
>[0];
|
||||
type OpenJellyfinSetupWindowMainDeps = Parameters<
|
||||
typeof createBuildOpenJellyfinSetupWindowMainDepsHandler
|
||||
>[0];
|
||||
|
||||
export type JellyfinRuntimeComposerOptions = ComposerInputs<{
|
||||
getResolvedJellyfinConfigMainDeps: Parameters<
|
||||
typeof createBuildGetResolvedJellyfinConfigMainDepsHandler
|
||||
>[0];
|
||||
getJellyfinClientInfoMainDeps: Parameters<
|
||||
typeof createBuildGetJellyfinClientInfoMainDepsHandler
|
||||
>[0];
|
||||
waitForMpvConnectedMainDeps: Parameters<typeof createBuildWaitForMpvConnectedMainDepsHandler>[0];
|
||||
launchMpvIdleForJellyfinPlaybackMainDeps: Parameters<
|
||||
typeof createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler
|
||||
>[0];
|
||||
ensureMpvConnectedForJellyfinPlaybackMainDeps: Omit<
|
||||
EnsureMpvConnectedMainDeps,
|
||||
'waitForMpvConnected' | 'launchMpvIdleForJellyfinPlayback'
|
||||
>;
|
||||
preloadJellyfinExternalSubtitlesMainDeps: Parameters<
|
||||
typeof createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler
|
||||
>[0];
|
||||
playJellyfinItemInMpvMainDeps: Omit<
|
||||
PlayJellyfinItemMainDeps,
|
||||
'ensureMpvConnectedForPlayback' | 'preloadExternalSubtitles'
|
||||
>;
|
||||
remoteComposerOptions: Omit<
|
||||
JellyfinRemoteComposerOptions,
|
||||
'getClientInfo' | 'getJellyfinConfig' | 'playJellyfinItem'
|
||||
>;
|
||||
handleJellyfinAuthCommandsMainDeps: Parameters<
|
||||
typeof createBuildHandleJellyfinAuthCommandsMainDepsHandler
|
||||
>[0];
|
||||
handleJellyfinListCommandsMainDeps: Parameters<
|
||||
typeof createBuildHandleJellyfinListCommandsMainDepsHandler
|
||||
>[0];
|
||||
handleJellyfinPlayCommandMainDeps: Omit<HandlePlayCommandMainDeps, 'playJellyfinItemInMpv'>;
|
||||
handleJellyfinRemoteAnnounceCommandMainDeps: Omit<
|
||||
HandleRemoteAnnounceMainDeps,
|
||||
'startJellyfinRemoteSession'
|
||||
>;
|
||||
startJellyfinRemoteSessionMainDeps: Omit<
|
||||
StartRemoteSessionMainDeps,
|
||||
'getJellyfinConfig' | 'handlePlay' | 'handlePlaystate' | 'handleGeneralCommand'
|
||||
>;
|
||||
stopJellyfinRemoteSessionMainDeps: Parameters<
|
||||
typeof createBuildStopJellyfinRemoteSessionMainDepsHandler
|
||||
>[0];
|
||||
runJellyfinCommandMainDeps: Omit<
|
||||
RunJellyfinCommandMainDeps,
|
||||
| 'getJellyfinConfig'
|
||||
| 'getJellyfinClientInfo'
|
||||
| 'handleAuthCommands'
|
||||
| 'handleRemoteAnnounceCommand'
|
||||
| 'handleListCommands'
|
||||
| 'handlePlayCommand'
|
||||
>;
|
||||
maybeFocusExistingJellyfinSetupWindowMainDeps: Parameters<
|
||||
typeof createMaybeFocusExistingJellyfinSetupWindowHandler
|
||||
>[0];
|
||||
openJellyfinSetupWindowMainDeps: Omit<
|
||||
OpenJellyfinSetupWindowMainDeps,
|
||||
'maybeFocusExistingSetupWindow' | 'getResolvedJellyfinConfig' | 'getJellyfinClientInfo'
|
||||
>;
|
||||
}>;
|
||||
|
||||
export type JellyfinRuntimeComposerResult = ComposerOutputs<{
|
||||
getResolvedJellyfinConfig: ReturnType<typeof createGetResolvedJellyfinConfigHandler>;
|
||||
getJellyfinClientInfo: ReturnType<typeof createGetJellyfinClientInfoHandler>;
|
||||
reportJellyfinRemoteProgress: ReturnType<
|
||||
typeof composeJellyfinRemoteHandlers
|
||||
>['reportJellyfinRemoteProgress'];
|
||||
reportJellyfinRemoteStopped: ReturnType<
|
||||
typeof composeJellyfinRemoteHandlers
|
||||
>['reportJellyfinRemoteStopped'];
|
||||
handleJellyfinRemotePlay: ReturnType<
|
||||
typeof composeJellyfinRemoteHandlers
|
||||
>['handleJellyfinRemotePlay'];
|
||||
handleJellyfinRemotePlaystate: ReturnType<
|
||||
typeof composeJellyfinRemoteHandlers
|
||||
>['handleJellyfinRemotePlaystate'];
|
||||
handleJellyfinRemoteGeneralCommand: ReturnType<
|
||||
typeof composeJellyfinRemoteHandlers
|
||||
>['handleJellyfinRemoteGeneralCommand'];
|
||||
playJellyfinItemInMpv: ReturnType<typeof createPlayJellyfinItemInMpvHandler>;
|
||||
startJellyfinRemoteSession: ReturnType<typeof createStartJellyfinRemoteSessionHandler>;
|
||||
stopJellyfinRemoteSession: ReturnType<typeof createStopJellyfinRemoteSessionHandler>;
|
||||
runJellyfinCommand: ReturnType<typeof createRunJellyfinCommandHandler>;
|
||||
openJellyfinSetupWindow: ReturnType<typeof createOpenJellyfinSetupWindowHandler>;
|
||||
}>;
|
||||
|
||||
export function composeJellyfinRuntimeHandlers(
|
||||
options: JellyfinRuntimeComposerOptions,
|
||||
): JellyfinRuntimeComposerResult {
|
||||
const getResolvedJellyfinConfig = createGetResolvedJellyfinConfigHandler(
|
||||
createBuildGetResolvedJellyfinConfigMainDepsHandler(
|
||||
options.getResolvedJellyfinConfigMainDeps,
|
||||
)(),
|
||||
);
|
||||
const getJellyfinClientInfo = createGetJellyfinClientInfoHandler(
|
||||
createBuildGetJellyfinClientInfoMainDepsHandler(options.getJellyfinClientInfoMainDeps)(),
|
||||
);
|
||||
|
||||
const waitForMpvConnected = createWaitForMpvConnectedHandler(
|
||||
createBuildWaitForMpvConnectedMainDepsHandler(options.waitForMpvConnectedMainDeps)(),
|
||||
);
|
||||
const launchMpvIdleForJellyfinPlayback = createLaunchMpvIdleForJellyfinPlaybackHandler(
|
||||
createBuildLaunchMpvIdleForJellyfinPlaybackMainDepsHandler(
|
||||
options.launchMpvIdleForJellyfinPlaybackMainDeps,
|
||||
)(),
|
||||
);
|
||||
const ensureMpvConnectedForJellyfinPlayback = createEnsureMpvConnectedForJellyfinPlaybackHandler(
|
||||
createBuildEnsureMpvConnectedForJellyfinPlaybackMainDepsHandler({
|
||||
...options.ensureMpvConnectedForJellyfinPlaybackMainDeps,
|
||||
waitForMpvConnected: (timeoutMs) => waitForMpvConnected(timeoutMs),
|
||||
launchMpvIdleForJellyfinPlayback: () => launchMpvIdleForJellyfinPlayback(),
|
||||
})(),
|
||||
);
|
||||
|
||||
const preloadJellyfinExternalSubtitles = createPreloadJellyfinExternalSubtitlesHandler(
|
||||
createBuildPreloadJellyfinExternalSubtitlesMainDepsHandler(
|
||||
options.preloadJellyfinExternalSubtitlesMainDeps,
|
||||
)(),
|
||||
);
|
||||
const playJellyfinItemInMpv = createPlayJellyfinItemInMpvHandler(
|
||||
createBuildPlayJellyfinItemInMpvMainDepsHandler({
|
||||
...options.playJellyfinItemInMpvMainDeps,
|
||||
ensureMpvConnectedForPlayback: () => ensureMpvConnectedForJellyfinPlayback(),
|
||||
preloadExternalSubtitles: (params) => {
|
||||
void preloadJellyfinExternalSubtitles(params);
|
||||
},
|
||||
})(),
|
||||
);
|
||||
|
||||
const {
|
||||
reportJellyfinRemoteProgress,
|
||||
reportJellyfinRemoteStopped,
|
||||
handleJellyfinRemotePlay,
|
||||
handleJellyfinRemotePlaystate,
|
||||
handleJellyfinRemoteGeneralCommand,
|
||||
} = composeJellyfinRemoteHandlers({
|
||||
...options.remoteComposerOptions,
|
||||
getClientInfo: () => getJellyfinClientInfo(),
|
||||
getJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
playJellyfinItem: (params) =>
|
||||
playJellyfinItemInMpv(params as Parameters<typeof playJellyfinItemInMpv>[0]),
|
||||
});
|
||||
|
||||
const handleJellyfinAuthCommands = createHandleJellyfinAuthCommands(
|
||||
createBuildHandleJellyfinAuthCommandsMainDepsHandler(
|
||||
options.handleJellyfinAuthCommandsMainDeps,
|
||||
)(),
|
||||
);
|
||||
const handleJellyfinListCommands = createHandleJellyfinListCommands(
|
||||
createBuildHandleJellyfinListCommandsMainDepsHandler(
|
||||
options.handleJellyfinListCommandsMainDeps,
|
||||
)(),
|
||||
);
|
||||
const handleJellyfinPlayCommand = createHandleJellyfinPlayCommand(
|
||||
createBuildHandleJellyfinPlayCommandMainDepsHandler({
|
||||
...options.handleJellyfinPlayCommandMainDeps,
|
||||
playJellyfinItemInMpv: (params) =>
|
||||
playJellyfinItemInMpv(params as Parameters<typeof playJellyfinItemInMpv>[0]),
|
||||
})(),
|
||||
);
|
||||
|
||||
let startJellyfinRemoteSession!: ReturnType<typeof createStartJellyfinRemoteSessionHandler>;
|
||||
const handleJellyfinRemoteAnnounceCommand = createHandleJellyfinRemoteAnnounceCommand(
|
||||
createBuildHandleJellyfinRemoteAnnounceCommandMainDepsHandler({
|
||||
...options.handleJellyfinRemoteAnnounceCommandMainDeps,
|
||||
startJellyfinRemoteSession: () => startJellyfinRemoteSession(),
|
||||
})(),
|
||||
);
|
||||
|
||||
startJellyfinRemoteSession = createStartJellyfinRemoteSessionHandler(
|
||||
createBuildStartJellyfinRemoteSessionMainDepsHandler({
|
||||
...options.startJellyfinRemoteSessionMainDeps,
|
||||
getJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
handlePlay: (payload) => handleJellyfinRemotePlay(payload),
|
||||
handlePlaystate: (payload) => handleJellyfinRemotePlaystate(payload),
|
||||
handleGeneralCommand: (payload) => handleJellyfinRemoteGeneralCommand(payload),
|
||||
})(),
|
||||
);
|
||||
|
||||
const stopJellyfinRemoteSession = createStopJellyfinRemoteSessionHandler(
|
||||
createBuildStopJellyfinRemoteSessionMainDepsHandler(
|
||||
options.stopJellyfinRemoteSessionMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
const runJellyfinCommand = createRunJellyfinCommandHandler(
|
||||
createBuildRunJellyfinCommandMainDepsHandler({
|
||||
...options.runJellyfinCommandMainDeps,
|
||||
getJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getJellyfinClientInfo: (jellyfinConfig) => getJellyfinClientInfo(jellyfinConfig),
|
||||
handleAuthCommands: (params) => handleJellyfinAuthCommands(params),
|
||||
handleRemoteAnnounceCommand: (args) => handleJellyfinRemoteAnnounceCommand(args),
|
||||
handleListCommands: (params) => handleJellyfinListCommands(params),
|
||||
handlePlayCommand: (params) => handleJellyfinPlayCommand(params),
|
||||
})(),
|
||||
);
|
||||
|
||||
const maybeFocusExistingJellyfinSetupWindow = createMaybeFocusExistingJellyfinSetupWindowHandler(
|
||||
options.maybeFocusExistingJellyfinSetupWindowMainDeps,
|
||||
);
|
||||
const openJellyfinSetupWindow = createOpenJellyfinSetupWindowHandler(
|
||||
createBuildOpenJellyfinSetupWindowMainDepsHandler({
|
||||
...options.openJellyfinSetupWindowMainDeps,
|
||||
maybeFocusExistingSetupWindow: maybeFocusExistingJellyfinSetupWindow,
|
||||
getResolvedJellyfinConfig: () => getResolvedJellyfinConfig(),
|
||||
getJellyfinClientInfo: () => getJellyfinClientInfo(),
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
getResolvedJellyfinConfig,
|
||||
getJellyfinClientInfo,
|
||||
reportJellyfinRemoteProgress,
|
||||
reportJellyfinRemoteStopped,
|
||||
handleJellyfinRemotePlay,
|
||||
handleJellyfinRemotePlaystate,
|
||||
handleJellyfinRemoteGeneralCommand,
|
||||
playJellyfinItemInMpv,
|
||||
startJellyfinRemoteSession,
|
||||
stopJellyfinRemoteSession,
|
||||
runJellyfinCommand,
|
||||
openJellyfinSetupWindow,
|
||||
};
|
||||
}
|
||||
|
||||
export { buildJellyfinSetupFormHtml, parseJellyfinSetupSubmissionUrl };
|
||||
219
src/main/runtime/composers/mpv-runtime-composer.test.ts
Normal file
219
src/main/runtime/composers/mpv-runtime-composer.test.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import type { MpvSubtitleRenderMetrics } from '../../../types';
|
||||
import { composeMpvRuntimeHandlers } from './mpv-runtime-composer';
|
||||
|
||||
const BASE_METRICS: MpvSubtitleRenderMetrics = {
|
||||
subPos: 100,
|
||||
subFontSize: 36,
|
||||
subScale: 1,
|
||||
subMarginY: 0,
|
||||
subMarginX: 0,
|
||||
subFont: '',
|
||||
subSpacing: 0,
|
||||
subBold: false,
|
||||
subItalic: false,
|
||||
subBorderSize: 0,
|
||||
subShadowOffset: 0,
|
||||
subAssOverride: 'yes',
|
||||
subScaleByWindow: true,
|
||||
subUseMargins: true,
|
||||
osdHeight: 0,
|
||||
osdDimensions: null,
|
||||
};
|
||||
|
||||
test('composeMpvRuntimeHandlers returns callable handlers and forwards to injected deps', async () => {
|
||||
const calls: string[] = [];
|
||||
let started = false;
|
||||
let metrics = BASE_METRICS;
|
||||
|
||||
class FakeMpvClient {
|
||||
connected = false;
|
||||
|
||||
constructor(
|
||||
public socketPath: string,
|
||||
public options: unknown,
|
||||
) {
|
||||
const autoStartOverlay = (options as { autoStartOverlay: boolean }).autoStartOverlay;
|
||||
calls.push(`create-client:${socketPath}`);
|
||||
calls.push(`auto-start:${String(autoStartOverlay)}`);
|
||||
}
|
||||
|
||||
on(): void {}
|
||||
|
||||
connect(): void {
|
||||
this.connected = true;
|
||||
calls.push('client-connect');
|
||||
}
|
||||
}
|
||||
|
||||
const composed = composeMpvRuntimeHandlers<
|
||||
FakeMpvClient,
|
||||
{ isKnownWord: (text: string) => boolean },
|
||||
{ text: string }
|
||||
>({
|
||||
bindMpvMainEventHandlersMainDeps: {
|
||||
appState: {
|
||||
initialArgs: null,
|
||||
overlayRuntimeInitialized: true,
|
||||
mpvClient: null,
|
||||
immersionTracker: null,
|
||||
subtitleTimingTracker: null,
|
||||
currentSubText: '',
|
||||
currentSubAssText: '',
|
||||
playbackPaused: null,
|
||||
previousSecondarySubVisibility: null,
|
||||
},
|
||||
getQuitOnDisconnectArmed: () => false,
|
||||
scheduleQuitCheck: () => {},
|
||||
quitApp: () => {},
|
||||
reportJellyfinRemoteStopped: () => {},
|
||||
maybeRunAnilistPostWatchUpdate: async () => {},
|
||||
logSubtitleTimingError: () => {},
|
||||
broadcastToOverlayWindows: () => {},
|
||||
onSubtitleChange: () => {},
|
||||
refreshDiscordPresence: () => {},
|
||||
updateCurrentMediaPath: () => {},
|
||||
getCurrentAnilistMediaKey: () => null,
|
||||
resetAnilistMediaTracking: () => {},
|
||||
maybeProbeAnilistDuration: () => {},
|
||||
ensureAnilistMediaGuess: () => {},
|
||||
syncImmersionMediaState: () => {},
|
||||
updateCurrentMediaTitle: () => {},
|
||||
resetAnilistMediaGuessState: () => {},
|
||||
reportJellyfinRemoteProgress: () => {},
|
||||
updateSubtitleRenderMetrics: () => {},
|
||||
},
|
||||
mpvClientRuntimeServiceFactoryMainDeps: {
|
||||
createClient: FakeMpvClient,
|
||||
getSocketPath: () => '/tmp/mpv.sock',
|
||||
getResolvedConfig: () => ({ auto_start_overlay: false }),
|
||||
isAutoStartOverlayEnabled: () => true,
|
||||
setOverlayVisible: () => {},
|
||||
shouldBindVisibleOverlayToMpvSubVisibility: () => true,
|
||||
isVisibleOverlayVisible: () => false,
|
||||
getReconnectTimer: () => null,
|
||||
setReconnectTimer: () => {},
|
||||
},
|
||||
updateMpvSubtitleRenderMetricsMainDeps: {
|
||||
getCurrentMetrics: () => metrics,
|
||||
setCurrentMetrics: (next) => {
|
||||
metrics = next;
|
||||
calls.push('set-metrics');
|
||||
},
|
||||
applyPatch: (current, patch) => {
|
||||
calls.push('apply-metrics-patch');
|
||||
return { next: { ...current, ...patch }, changed: true };
|
||||
},
|
||||
broadcastMetrics: () => {
|
||||
calls.push('broadcast-metrics');
|
||||
},
|
||||
},
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: {
|
||||
getYomitanExt: () => null,
|
||||
getYomitanParserWindow: () => null,
|
||||
setYomitanParserWindow: () => {},
|
||||
getYomitanParserReadyPromise: () => null,
|
||||
setYomitanParserReadyPromise: () => {},
|
||||
getYomitanParserInitPromise: () => null,
|
||||
setYomitanParserInitPromise: () => {},
|
||||
isKnownWord: (text) => text === 'known',
|
||||
recordLookup: () => {},
|
||||
getKnownWordMatchMode: () => 'headword',
|
||||
getMinSentenceWordsForNPlusOne: () => 3,
|
||||
getJlptLevel: () => null,
|
||||
getJlptEnabled: () => true,
|
||||
getFrequencyDictionaryEnabled: () => true,
|
||||
getFrequencyRank: () => null,
|
||||
getYomitanGroupDebugEnabled: () => false,
|
||||
getMecabTokenizer: () => null,
|
||||
},
|
||||
createTokenizerRuntimeDeps: (deps) => {
|
||||
calls.push('create-tokenizer-runtime-deps');
|
||||
return { isKnownWord: (text: string) => deps.isKnownWord(text) };
|
||||
},
|
||||
tokenizeSubtitle: async (text, deps) => {
|
||||
calls.push(`tokenize:${text}`);
|
||||
deps.isKnownWord('known');
|
||||
return { text };
|
||||
},
|
||||
createMecabTokenizerAndCheckMainDeps: {
|
||||
getMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
setMecabTokenizer: () => {},
|
||||
createMecabTokenizer: () => ({ id: 'mecab' }),
|
||||
checkAvailability: async () => {
|
||||
calls.push('check-mecab');
|
||||
},
|
||||
},
|
||||
prewarmSubtitleDictionariesMainDeps: {
|
||||
ensureJlptDictionaryLookup: async () => {
|
||||
calls.push('prewarm-jlpt');
|
||||
},
|
||||
ensureFrequencyDictionaryLookup: async () => {
|
||||
calls.push('prewarm-frequency');
|
||||
},
|
||||
},
|
||||
},
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: {
|
||||
now: () => 100,
|
||||
logDebug: () => {
|
||||
calls.push('warmup-debug');
|
||||
},
|
||||
logWarn: () => {
|
||||
calls.push('warmup-warn');
|
||||
},
|
||||
},
|
||||
startBackgroundWarmupsMainDeps: {
|
||||
getStarted: () => started,
|
||||
setStarted: (next) => {
|
||||
started = next;
|
||||
calls.push(`set-started:${String(next)}`);
|
||||
},
|
||||
isTexthookerOnlyMode: () => false,
|
||||
ensureYomitanExtensionLoaded: async () => {
|
||||
calls.push('warmup-yomitan');
|
||||
},
|
||||
shouldAutoConnectJellyfinRemote: () => false,
|
||||
startJellyfinRemoteSession: async () => {
|
||||
calls.push('warmup-jellyfin');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.bindMpvClientEventHandlers, 'function');
|
||||
assert.equal(typeof composed.createMpvClientRuntimeService, 'function');
|
||||
assert.equal(typeof composed.updateMpvSubtitleRenderMetrics, 'function');
|
||||
assert.equal(typeof composed.tokenizeSubtitle, 'function');
|
||||
assert.equal(typeof composed.createMecabTokenizerAndCheck, 'function');
|
||||
assert.equal(typeof composed.prewarmSubtitleDictionaries, 'function');
|
||||
assert.equal(typeof composed.launchBackgroundWarmupTask, 'function');
|
||||
assert.equal(typeof composed.startBackgroundWarmups, 'function');
|
||||
|
||||
const client = composed.createMpvClientRuntimeService();
|
||||
assert.equal(client.connected, true);
|
||||
|
||||
composed.updateMpvSubtitleRenderMetrics({ subPos: 90 });
|
||||
const tokenized = await composed.tokenizeSubtitle('subtitle text');
|
||||
await composed.createMecabTokenizerAndCheck();
|
||||
await composed.prewarmSubtitleDictionaries();
|
||||
composed.startBackgroundWarmups();
|
||||
|
||||
assert.deepEqual(tokenized, { text: 'subtitle text' });
|
||||
assert.equal(metrics.subPos, 90);
|
||||
assert.ok(calls.includes('create-client:/tmp/mpv.sock'));
|
||||
assert.ok(calls.includes('auto-start:true'));
|
||||
assert.ok(calls.includes('client-connect'));
|
||||
assert.ok(calls.includes('apply-metrics-patch'));
|
||||
assert.ok(calls.includes('set-metrics'));
|
||||
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('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'));
|
||||
});
|
||||
167
src/main/runtime/composers/mpv-runtime-composer.ts
Normal file
167
src/main/runtime/composers/mpv-runtime-composer.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { createBindMpvMainEventHandlersHandler } from '../mpv-main-event-bindings';
|
||||
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-event-main-deps';
|
||||
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
|
||||
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
|
||||
import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service';
|
||||
import type { Config } from '../../../types';
|
||||
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
|
||||
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
|
||||
import {
|
||||
createBuildTokenizerDepsMainHandler,
|
||||
createCreateMecabTokenizerAndCheckMainHandler,
|
||||
createPrewarmSubtitleDictionariesMainHandler,
|
||||
} from '../subtitle-tokenization-main-deps';
|
||||
import {
|
||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler,
|
||||
createBuildStartBackgroundWarmupsMainDepsHandler,
|
||||
} from '../startup-warmups-main-deps';
|
||||
import {
|
||||
createLaunchBackgroundWarmupTaskHandler as createLaunchBackgroundWarmupTaskFromStartup,
|
||||
createStartBackgroundWarmupsHandler as createStartBackgroundWarmupsFromStartup,
|
||||
} from '../startup-warmups';
|
||||
import type { BuiltMainDeps, ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type BindMpvMainEventHandlersMainDeps = Parameters<
|
||||
typeof createBuildBindMpvMainEventHandlersMainDepsHandler
|
||||
>[0];
|
||||
type BindMpvMainEventHandlers = ReturnType<typeof createBindMpvMainEventHandlersHandler>;
|
||||
type BoundMpvClient = Parameters<BindMpvMainEventHandlers>[0];
|
||||
type RuntimeMpvClient = BoundMpvClient & { connect: () => void };
|
||||
type MpvClientRuntimeServiceFactoryMainDeps<TMpvClient extends RuntimeMpvClient> = Omit<
|
||||
Parameters<
|
||||
typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>
|
||||
>[0],
|
||||
'bindEventHandlers'
|
||||
>;
|
||||
type UpdateMpvSubtitleRenderMetricsMainDeps = Parameters<
|
||||
typeof createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler
|
||||
>[0];
|
||||
type BuildTokenizerDepsMainDeps = Parameters<typeof createBuildTokenizerDepsMainHandler>[0];
|
||||
type TokenizerMainDeps = BuiltMainDeps<typeof createBuildTokenizerDepsMainHandler>;
|
||||
type CreateMecabTokenizerAndCheckMainDeps = Parameters<
|
||||
typeof createCreateMecabTokenizerAndCheckMainHandler
|
||||
>[0];
|
||||
type PrewarmSubtitleDictionariesMainDeps = Parameters<
|
||||
typeof createPrewarmSubtitleDictionariesMainHandler
|
||||
>[0];
|
||||
type LaunchBackgroundWarmupTaskMainDeps = Parameters<
|
||||
typeof createBuildLaunchBackgroundWarmupTaskMainDepsHandler
|
||||
>[0];
|
||||
type StartBackgroundWarmupsMainDeps = Omit<
|
||||
Parameters<typeof createBuildStartBackgroundWarmupsMainDepsHandler>[0],
|
||||
'launchTask' | 'createMecabTokenizerAndCheck' | 'prewarmSubtitleDictionaries'
|
||||
>;
|
||||
|
||||
export type MpvRuntimeComposerOptions<
|
||||
TMpvClient extends RuntimeMpvClient,
|
||||
TTokenizerRuntimeDeps,
|
||||
TTokenizedSubtitle,
|
||||
> = ComposerInputs<{
|
||||
bindMpvMainEventHandlersMainDeps: BindMpvMainEventHandlersMainDeps;
|
||||
mpvClientRuntimeServiceFactoryMainDeps: MpvClientRuntimeServiceFactoryMainDeps<TMpvClient>;
|
||||
updateMpvSubtitleRenderMetricsMainDeps: UpdateMpvSubtitleRenderMetricsMainDeps;
|
||||
tokenizer: {
|
||||
buildTokenizerDepsMainDeps: BuildTokenizerDepsMainDeps;
|
||||
createTokenizerRuntimeDeps: (deps: TokenizerMainDeps) => TTokenizerRuntimeDeps;
|
||||
tokenizeSubtitle: (text: string, deps: TTokenizerRuntimeDeps) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheckMainDeps: CreateMecabTokenizerAndCheckMainDeps;
|
||||
prewarmSubtitleDictionariesMainDeps: PrewarmSubtitleDictionariesMainDeps;
|
||||
};
|
||||
warmups: {
|
||||
launchBackgroundWarmupTaskMainDeps: LaunchBackgroundWarmupTaskMainDeps;
|
||||
startBackgroundWarmupsMainDeps: StartBackgroundWarmupsMainDeps;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type MpvRuntimeComposerResult<
|
||||
TMpvClient extends RuntimeMpvClient,
|
||||
TTokenizedSubtitle,
|
||||
> = ComposerOutputs<{
|
||||
bindMpvClientEventHandlers: BindMpvMainEventHandlers;
|
||||
createMpvClientRuntimeService: () => TMpvClient;
|
||||
updateMpvSubtitleRenderMetrics: ReturnType<typeof createUpdateMpvSubtitleRenderMetricsHandler>;
|
||||
tokenizeSubtitle: (text: string) => Promise<TTokenizedSubtitle>;
|
||||
createMecabTokenizerAndCheck: () => Promise<void>;
|
||||
prewarmSubtitleDictionaries: () => Promise<void>;
|
||||
launchBackgroundWarmupTask: ReturnType<typeof createLaunchBackgroundWarmupTaskFromStartup>;
|
||||
startBackgroundWarmups: ReturnType<typeof createStartBackgroundWarmupsFromStartup>;
|
||||
}>;
|
||||
|
||||
export function composeMpvRuntimeHandlers<
|
||||
TMpvClient extends RuntimeMpvClient,
|
||||
TTokenizerRuntimeDeps,
|
||||
TTokenizedSubtitle,
|
||||
>(
|
||||
options: MpvRuntimeComposerOptions<TMpvClient, TTokenizerRuntimeDeps, TTokenizedSubtitle>,
|
||||
): MpvRuntimeComposerResult<TMpvClient, TTokenizedSubtitle> {
|
||||
const bindMpvMainEventHandlersMainDeps = createBuildBindMpvMainEventHandlersMainDepsHandler(
|
||||
options.bindMpvMainEventHandlersMainDeps,
|
||||
)();
|
||||
const bindMpvClientEventHandlers = createBindMpvMainEventHandlersHandler(
|
||||
bindMpvMainEventHandlersMainDeps,
|
||||
);
|
||||
|
||||
const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
|
||||
createBuildMpvClientRuntimeServiceFactoryDepsHandler<
|
||||
TMpvClient,
|
||||
Config,
|
||||
MpvClientRuntimeServiceOptions
|
||||
>({
|
||||
...options.mpvClientRuntimeServiceFactoryMainDeps,
|
||||
bindEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
});
|
||||
const createMpvClientRuntimeService = (): TMpvClient =>
|
||||
createMpvClientRuntimeServiceFactory(buildMpvClientRuntimeServiceFactoryMainDepsHandler())();
|
||||
|
||||
const updateMpvSubtitleRenderMetrics = createUpdateMpvSubtitleRenderMetricsHandler(
|
||||
createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler(
|
||||
options.updateMpvSubtitleRenderMetricsMainDeps,
|
||||
)(),
|
||||
);
|
||||
|
||||
const buildTokenizerDepsHandler = createBuildTokenizerDepsMainHandler(
|
||||
options.tokenizer.buildTokenizerDepsMainDeps,
|
||||
);
|
||||
const createMecabTokenizerAndCheck = createCreateMecabTokenizerAndCheckMainHandler(
|
||||
options.tokenizer.createMecabTokenizerAndCheckMainDeps,
|
||||
);
|
||||
const prewarmSubtitleDictionaries = createPrewarmSubtitleDictionariesMainHandler(
|
||||
options.tokenizer.prewarmSubtitleDictionariesMainDeps,
|
||||
);
|
||||
const tokenizeSubtitle = async (text: string): Promise<TTokenizedSubtitle> => {
|
||||
await prewarmSubtitleDictionaries();
|
||||
return options.tokenizer.tokenizeSubtitle(
|
||||
text,
|
||||
options.tokenizer.createTokenizerRuntimeDeps(buildTokenizerDepsHandler()),
|
||||
);
|
||||
};
|
||||
|
||||
const launchBackgroundWarmupTask = createLaunchBackgroundWarmupTaskFromStartup(
|
||||
createBuildLaunchBackgroundWarmupTaskMainDepsHandler(
|
||||
options.warmups.launchBackgroundWarmupTaskMainDeps,
|
||||
)(),
|
||||
);
|
||||
const startBackgroundWarmups = createStartBackgroundWarmupsFromStartup(
|
||||
createBuildStartBackgroundWarmupsMainDepsHandler({
|
||||
...options.warmups.startBackgroundWarmupsMainDeps,
|
||||
launchTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
})(),
|
||||
);
|
||||
|
||||
return {
|
||||
bindMpvClientEventHandlers: (client) => bindMpvClientEventHandlers(client),
|
||||
createMpvClientRuntimeService,
|
||||
updateMpvSubtitleRenderMetrics: (patch) => updateMpvSubtitleRenderMetrics(patch),
|
||||
tokenizeSubtitle,
|
||||
createMecabTokenizerAndCheck: () => createMecabTokenizerAndCheck(),
|
||||
prewarmSubtitleDictionaries: () => prewarmSubtitleDictionaries(),
|
||||
launchBackgroundWarmupTask: (label, task) => launchBackgroundWarmupTask(label, task),
|
||||
startBackgroundWarmups: () => startBackgroundWarmups(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeShortcutRuntimes } from './shortcuts-runtime-composer';
|
||||
|
||||
test('composeShortcutRuntimes returns callable shortcut runtime handlers', () => {
|
||||
const composed = composeShortcutRuntimes({
|
||||
globalShortcuts: {
|
||||
getConfiguredShortcutsMainDeps: {
|
||||
getResolvedConfig: () => ({}) as never,
|
||||
defaultConfig: {} as never,
|
||||
resolveConfiguredShortcuts: () => ({}) as never,
|
||||
},
|
||||
buildRegisterGlobalShortcutsMainDeps: () => ({
|
||||
getConfiguredShortcuts: () => ({}) as never,
|
||||
registerGlobalShortcutsCore: () => {},
|
||||
toggleVisibleOverlay: () => {},
|
||||
toggleInvisibleOverlay: () => {},
|
||||
openYomitanSettings: () => {},
|
||||
isDev: false,
|
||||
getMainWindow: () => null,
|
||||
}),
|
||||
buildRefreshGlobalAndOverlayShortcutsMainDeps: () => ({
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
registerGlobalShortcuts: () => {},
|
||||
syncOverlayShortcuts: () => {},
|
||||
}),
|
||||
},
|
||||
numericShortcutRuntimeMainDeps: {
|
||||
globalShortcut: {
|
||||
register: () => true,
|
||||
unregister: () => {},
|
||||
},
|
||||
showMpvOsd: () => {},
|
||||
setTimer: (handler, timeoutMs) => setTimeout(handler, timeoutMs),
|
||||
clearTimer: (timer) => clearTimeout(timer),
|
||||
},
|
||||
numericSessions: {
|
||||
onMultiCopyDigit: () => {},
|
||||
onMineSentenceDigit: () => {},
|
||||
},
|
||||
overlayShortcutsRuntimeMainDeps: {
|
||||
overlayShortcutsRuntime: {
|
||||
registerOverlayShortcuts: () => {},
|
||||
unregisterOverlayShortcuts: () => {},
|
||||
syncOverlayShortcuts: () => {},
|
||||
refreshOverlayShortcuts: () => {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.getConfiguredShortcuts, 'function');
|
||||
assert.equal(typeof composed.registerGlobalShortcuts, 'function');
|
||||
assert.equal(typeof composed.refreshGlobalAndOverlayShortcuts, 'function');
|
||||
assert.equal(typeof composed.cancelPendingMultiCopy, 'function');
|
||||
assert.equal(typeof composed.startPendingMultiCopy, 'function');
|
||||
assert.equal(typeof composed.cancelPendingMineSentenceMultiple, 'function');
|
||||
assert.equal(typeof composed.startPendingMineSentenceMultiple, 'function');
|
||||
assert.equal(typeof composed.registerOverlayShortcuts, 'function');
|
||||
assert.equal(typeof composed.unregisterOverlayShortcuts, 'function');
|
||||
assert.equal(typeof composed.syncOverlayShortcuts, 'function');
|
||||
assert.equal(typeof composed.refreshOverlayShortcuts, 'function');
|
||||
});
|
||||
60
src/main/runtime/composers/shortcuts-runtime-composer.ts
Normal file
60
src/main/runtime/composers/shortcuts-runtime-composer.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createNumericShortcutRuntime } from '../../../core/services/numeric-shortcut';
|
||||
import {
|
||||
createBuildNumericShortcutRuntimeMainDepsHandler,
|
||||
createGlobalShortcutsRuntimeHandlers,
|
||||
createNumericShortcutSessionRuntimeHandlers,
|
||||
createOverlayShortcutsRuntimeHandlers,
|
||||
} from '../domains/shortcuts';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type GlobalShortcutsOptions = Parameters<typeof createGlobalShortcutsRuntimeHandlers>[0];
|
||||
type NumericShortcutRuntimeMainDeps = Parameters<
|
||||
typeof createBuildNumericShortcutRuntimeMainDepsHandler
|
||||
>[0];
|
||||
type NumericSessionOptions = Omit<
|
||||
Parameters<typeof createNumericShortcutSessionRuntimeHandlers>[0],
|
||||
'multiCopySession' | 'mineSentenceSession'
|
||||
>;
|
||||
type OverlayShortcutsMainDeps = Parameters<
|
||||
typeof createOverlayShortcutsRuntimeHandlers
|
||||
>[0]['overlayShortcutsRuntimeMainDeps'];
|
||||
|
||||
export type ShortcutsRuntimeComposerOptions = ComposerInputs<{
|
||||
globalShortcuts: GlobalShortcutsOptions;
|
||||
numericShortcutRuntimeMainDeps: NumericShortcutRuntimeMainDeps;
|
||||
numericSessions: NumericSessionOptions;
|
||||
overlayShortcutsRuntimeMainDeps: OverlayShortcutsMainDeps;
|
||||
}>;
|
||||
|
||||
export type ShortcutsRuntimeComposerResult = ComposerOutputs<
|
||||
ReturnType<typeof createGlobalShortcutsRuntimeHandlers> &
|
||||
ReturnType<typeof createNumericShortcutSessionRuntimeHandlers> &
|
||||
ReturnType<typeof createOverlayShortcutsRuntimeHandlers>
|
||||
>;
|
||||
|
||||
export function composeShortcutRuntimes(
|
||||
options: ShortcutsRuntimeComposerOptions,
|
||||
): ShortcutsRuntimeComposerResult {
|
||||
const globalShortcuts = createGlobalShortcutsRuntimeHandlers(options.globalShortcuts);
|
||||
|
||||
const numericShortcutRuntimeMainDeps = createBuildNumericShortcutRuntimeMainDepsHandler(
|
||||
options.numericShortcutRuntimeMainDeps,
|
||||
)();
|
||||
const numericShortcutRuntime = createNumericShortcutRuntime(numericShortcutRuntimeMainDeps);
|
||||
const numericSessions = createNumericShortcutSessionRuntimeHandlers({
|
||||
multiCopySession: numericShortcutRuntime.createSession(),
|
||||
mineSentenceSession: numericShortcutRuntime.createSession(),
|
||||
onMultiCopyDigit: options.numericSessions.onMultiCopyDigit,
|
||||
onMineSentenceDigit: options.numericSessions.onMineSentenceDigit,
|
||||
});
|
||||
|
||||
const overlayShortcuts = createOverlayShortcutsRuntimeHandlers({
|
||||
overlayShortcutsRuntimeMainDeps: options.overlayShortcutsRuntimeMainDeps,
|
||||
});
|
||||
|
||||
return {
|
||||
...globalShortcuts,
|
||||
...numericSessions,
|
||||
...overlayShortcuts,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { composeStartupLifecycleHandlers } from './startup-lifecycle-composer';
|
||||
|
||||
test('composeStartupLifecycleHandlers returns callable startup lifecycle handlers', () => {
|
||||
const composed = composeStartupLifecycleHandlers({
|
||||
registerProtocolUrlHandlersMainDeps: {
|
||||
registerOpenUrl: () => {},
|
||||
registerSecondInstance: () => {},
|
||||
handleAnilistSetupProtocolUrl: () => false,
|
||||
findAnilistSetupDeepLinkArgvUrl: () => null,
|
||||
logUnhandledOpenUrl: () => {},
|
||||
logUnhandledSecondInstanceUrl: () => {},
|
||||
},
|
||||
onWillQuitCleanupMainDeps: {
|
||||
destroyTray: () => {},
|
||||
stopConfigHotReload: () => {},
|
||||
restorePreviousSecondarySubVisibility: () => {},
|
||||
unregisterAllGlobalShortcuts: () => {},
|
||||
stopSubtitleWebsocket: () => {},
|
||||
stopTexthookerService: () => {},
|
||||
getYomitanParserWindow: () => null,
|
||||
clearYomitanParserState: () => {},
|
||||
getWindowTracker: () => null,
|
||||
flushMpvLog: () => {},
|
||||
getMpvSocket: () => null,
|
||||
getReconnectTimer: () => null,
|
||||
clearReconnectTimerRef: () => {},
|
||||
getSubtitleTimingTracker: () => null,
|
||||
getImmersionTracker: () => null,
|
||||
clearImmersionTracker: () => {},
|
||||
getAnkiIntegration: () => null,
|
||||
getAnilistSetupWindow: () => null,
|
||||
clearAnilistSetupWindow: () => {},
|
||||
getJellyfinSetupWindow: () => null,
|
||||
clearJellyfinSetupWindow: () => {},
|
||||
stopJellyfinRemoteSession: async () => {},
|
||||
stopDiscordPresenceService: () => {},
|
||||
},
|
||||
shouldRestoreWindowsOnActivateMainDeps: {
|
||||
isOverlayRuntimeInitialized: () => false,
|
||||
getAllWindowCount: () => 0,
|
||||
},
|
||||
restoreWindowsOnActivateMainDeps: {
|
||||
createMainWindow: () => {},
|
||||
createInvisibleWindow: () => {},
|
||||
updateVisibleOverlayVisibility: () => {},
|
||||
updateInvisibleOverlayVisibility: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(typeof composed.registerProtocolUrlHandlers, 'function');
|
||||
assert.equal(typeof composed.onWillQuitCleanup, 'function');
|
||||
assert.equal(typeof composed.shouldRestoreWindowsOnActivate, 'function');
|
||||
assert.equal(typeof composed.restoreWindowsOnActivate, 'function');
|
||||
});
|
||||
66
src/main/runtime/composers/startup-lifecycle-composer.ts
Normal file
66
src/main/runtime/composers/startup-lifecycle-composer.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
createOnWillQuitCleanupHandler,
|
||||
createRestoreWindowsOnActivateHandler,
|
||||
createShouldRestoreWindowsOnActivateHandler,
|
||||
} from '../app-lifecycle-actions';
|
||||
import { createBuildOnWillQuitCleanupDepsHandler } from '../app-lifecycle-main-cleanup';
|
||||
import {
|
||||
createBuildRestoreWindowsOnActivateMainDepsHandler,
|
||||
createBuildShouldRestoreWindowsOnActivateMainDepsHandler,
|
||||
} from '../app-lifecycle-main-activate';
|
||||
import { createBuildRegisterProtocolUrlHandlersMainDepsHandler } from '../protocol-url-handlers-main-deps';
|
||||
import { registerProtocolUrlHandlers } from '../protocol-url-handlers';
|
||||
import type { ComposerInputs, ComposerOutputs } from './contracts';
|
||||
|
||||
type RegisterProtocolUrlHandlersMainDeps = Parameters<
|
||||
typeof createBuildRegisterProtocolUrlHandlersMainDepsHandler
|
||||
>[0];
|
||||
type OnWillQuitCleanupDeps = Parameters<typeof createBuildOnWillQuitCleanupDepsHandler>[0];
|
||||
type ShouldRestoreWindowsOnActivateMainDeps = Parameters<
|
||||
typeof createBuildShouldRestoreWindowsOnActivateMainDepsHandler
|
||||
>[0];
|
||||
type RestoreWindowsOnActivateMainDeps = Parameters<
|
||||
typeof createBuildRestoreWindowsOnActivateMainDepsHandler
|
||||
>[0];
|
||||
|
||||
export type StartupLifecycleComposerOptions = ComposerInputs<{
|
||||
registerProtocolUrlHandlersMainDeps: RegisterProtocolUrlHandlersMainDeps;
|
||||
onWillQuitCleanupMainDeps: OnWillQuitCleanupDeps;
|
||||
shouldRestoreWindowsOnActivateMainDeps: ShouldRestoreWindowsOnActivateMainDeps;
|
||||
restoreWindowsOnActivateMainDeps: RestoreWindowsOnActivateMainDeps;
|
||||
}>;
|
||||
|
||||
export type StartupLifecycleComposerResult = ComposerOutputs<{
|
||||
registerProtocolUrlHandlers: () => void;
|
||||
onWillQuitCleanup: () => void;
|
||||
shouldRestoreWindowsOnActivate: () => boolean;
|
||||
restoreWindowsOnActivate: () => void;
|
||||
}>;
|
||||
|
||||
export function composeStartupLifecycleHandlers(
|
||||
options: StartupLifecycleComposerOptions,
|
||||
): StartupLifecycleComposerResult {
|
||||
const registerProtocolUrlHandlersMainDeps = createBuildRegisterProtocolUrlHandlersMainDepsHandler(
|
||||
options.registerProtocolUrlHandlersMainDeps,
|
||||
)();
|
||||
|
||||
const onWillQuitCleanupHandler = createOnWillQuitCleanupHandler(
|
||||
createBuildOnWillQuitCleanupDepsHandler(options.onWillQuitCleanupMainDeps)(),
|
||||
);
|
||||
const shouldRestoreWindowsOnActivateHandler = createShouldRestoreWindowsOnActivateHandler(
|
||||
createBuildShouldRestoreWindowsOnActivateMainDepsHandler(
|
||||
options.shouldRestoreWindowsOnActivateMainDeps,
|
||||
)(),
|
||||
);
|
||||
const restoreWindowsOnActivateHandler = createRestoreWindowsOnActivateHandler(
|
||||
createBuildRestoreWindowsOnActivateMainDepsHandler(options.restoreWindowsOnActivateMainDeps)(),
|
||||
);
|
||||
|
||||
return {
|
||||
registerProtocolUrlHandlers: () =>
|
||||
registerProtocolUrlHandlers(registerProtocolUrlHandlersMainDeps),
|
||||
onWillQuitCleanup: () => onWillQuitCleanupHandler(),
|
||||
shouldRestoreWindowsOnActivate: () => shouldRestoreWindowsOnActivateHandler(),
|
||||
restoreWindowsOnActivate: () => restoreWindowsOnActivateHandler(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user