mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-04 06:12:06 -07:00
fix: address latest coderabbit feedback
This commit is contained in:
@@ -74,7 +74,9 @@ export function createAnilistRuntimeCoordinator(input: AnilistRuntimeCoordinator
|
|||||||
const window = new BrowserWindow(options);
|
const window = new BrowserWindow(options);
|
||||||
input.appState.anilistSetupWindow = window;
|
input.appState.anilistSetupWindow = window;
|
||||||
window.on('closed', () => {
|
window.on('closed', () => {
|
||||||
input.appState.anilistSetupWindow = null;
|
if (input.appState.anilistSetupWindow === window) {
|
||||||
|
input.appState.anilistSetupWindow = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return window as unknown as AnilistSetupWindowLike;
|
return window as unknown as AnilistSetupWindowLike;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,3 +53,97 @@ test('discord presence lifecycle runtime starts service and publishes presence w
|
|||||||
|
|
||||||
assert.deepEqual(calls, ['start', 'Demo', 'publish']);
|
assert.deepEqual(calls, ['start', 'Demo', 'publish']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('discord presence lifecycle runtime stops the existing service before replacement', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let service: { start: () => Promise<void>; stop: () => Promise<void> } | null = {
|
||||||
|
start: async () => {
|
||||||
|
calls.push('old-start');
|
||||||
|
},
|
||||||
|
stop: async () => {
|
||||||
|
calls.push('old-stop');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = createDiscordPresenceLifecycleRuntime({
|
||||||
|
getResolvedConfig: () => ({ discordPresence: { enabled: true } }),
|
||||||
|
getDiscordPresenceService: () => service as never,
|
||||||
|
setDiscordPresenceService: (next) => {
|
||||||
|
service = next as typeof service;
|
||||||
|
},
|
||||||
|
getMpvClient: () => null,
|
||||||
|
getCurrentMediaTitle: () => 'Demo',
|
||||||
|
getCurrentMediaPath: () => '/tmp/demo.mkv',
|
||||||
|
getCurrentSubtitleText: () => 'subtitle',
|
||||||
|
getPlaybackPaused: () => false,
|
||||||
|
getFallbackMediaDurationSec: () => 12,
|
||||||
|
createDiscordPresenceService: () => ({
|
||||||
|
start: async () => {
|
||||||
|
calls.push('new-start');
|
||||||
|
},
|
||||||
|
stop: async () => {
|
||||||
|
calls.push('new-stop');
|
||||||
|
},
|
||||||
|
publish: () => {
|
||||||
|
calls.push('publish');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createDiscordRuntime: () => ({
|
||||||
|
refreshDiscordPresenceMediaDuration: async () => {},
|
||||||
|
publishDiscordPresence: () => {
|
||||||
|
calls.push('runtime-publish');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
now: () => 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.initializeDiscordPresenceService();
|
||||||
|
|
||||||
|
assert.deepEqual(calls, ['old-stop', 'new-start', 'runtime-publish']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('discord presence lifecycle runtime stops the existing service when disabled', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let service: { start: () => Promise<void>; stop: () => Promise<void> } | null = {
|
||||||
|
start: async () => {
|
||||||
|
calls.push('old-start');
|
||||||
|
},
|
||||||
|
stop: async () => {
|
||||||
|
calls.push('old-stop');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime = createDiscordPresenceLifecycleRuntime({
|
||||||
|
getResolvedConfig: () => ({ discordPresence: { enabled: false } }),
|
||||||
|
getDiscordPresenceService: () => service as never,
|
||||||
|
setDiscordPresenceService: (next) => {
|
||||||
|
service = next as typeof service;
|
||||||
|
},
|
||||||
|
getMpvClient: () => null,
|
||||||
|
getCurrentMediaTitle: () => 'Demo',
|
||||||
|
getCurrentMediaPath: () => '/tmp/demo.mkv',
|
||||||
|
getCurrentSubtitleText: () => 'subtitle',
|
||||||
|
getPlaybackPaused: () => false,
|
||||||
|
getFallbackMediaDurationSec: () => 12,
|
||||||
|
createDiscordPresenceService: () => {
|
||||||
|
calls.push('create');
|
||||||
|
return {
|
||||||
|
start: async () => {},
|
||||||
|
stop: async () => {},
|
||||||
|
publish: () => {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
createDiscordRuntime: () => ({
|
||||||
|
refreshDiscordPresenceMediaDuration: async () => {},
|
||||||
|
publishDiscordPresence: () => {
|
||||||
|
calls.push('runtime-publish');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
now: () => 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runtime.initializeDiscordPresenceService();
|
||||||
|
|
||||||
|
assert.equal(service, null);
|
||||||
|
assert.deepEqual(calls, ['old-stop']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export function createDiscordPresenceLifecycleRuntime(
|
|||||||
): DiscordPresenceLifecycleRuntime {
|
): DiscordPresenceLifecycleRuntime {
|
||||||
let discordPresenceMediaDurationSec: number | null = null;
|
let discordPresenceMediaDurationSec: number | null = null;
|
||||||
const discordPresenceSessionStartedAtMs = input.now ? input.now() : Date.now();
|
const discordPresenceSessionStartedAtMs = input.now ? input.now() : Date.now();
|
||||||
|
const stopCurrentDiscordPresenceService = async (): Promise<void> => {
|
||||||
|
await input.getDiscordPresenceService()?.stop?.();
|
||||||
|
input.setDiscordPresenceService(null);
|
||||||
|
};
|
||||||
|
|
||||||
const discordPresenceRuntime = (input.createDiscordRuntime ?? createDiscordPresenceRuntime)({
|
const discordPresenceRuntime = (input.createDiscordRuntime ?? createDiscordPresenceRuntime)({
|
||||||
getDiscordPresenceService: () => input.getDiscordPresenceService(),
|
getDiscordPresenceService: () => input.getDiscordPresenceService(),
|
||||||
@@ -72,19 +76,17 @@ export function createDiscordPresenceLifecycleRuntime(
|
|||||||
},
|
},
|
||||||
initializeDiscordPresenceService: async () => {
|
initializeDiscordPresenceService: async () => {
|
||||||
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
||||||
input.setDiscordPresenceService(null);
|
await stopCurrentDiscordPresenceService();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await stopCurrentDiscordPresenceService();
|
||||||
input.setDiscordPresenceService(
|
input.setDiscordPresenceService(
|
||||||
input.createDiscordPresenceService(input.getResolvedConfig().discordPresence),
|
input.createDiscordPresenceService(input.getResolvedConfig().discordPresence),
|
||||||
);
|
);
|
||||||
await input.getDiscordPresenceService()?.start();
|
await input.getDiscordPresenceService()?.start();
|
||||||
discordPresenceRuntime.publishDiscordPresence();
|
discordPresenceRuntime.publishDiscordPresence();
|
||||||
},
|
},
|
||||||
stopDiscordPresenceService: async () => {
|
stopDiscordPresenceService: stopCurrentDiscordPresenceService,
|
||||||
await input.getDiscordPresenceService()?.stop?.();
|
|
||||||
input.setDiscordPresenceService(null);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,17 @@ export async function runHeadlessKnownWordRefresh(input: {
|
|||||||
};
|
};
|
||||||
requestAppQuit: () => void;
|
requestAppQuit: () => void;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
if (input.resolvedConfig.ankiConnect.enabled !== true) {
|
const effectiveAnkiConfig =
|
||||||
|
input.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(input.resolvedConfig.ankiConnect) ??
|
||||||
|
input.resolvedConfig.ankiConnect;
|
||||||
|
|
||||||
|
if (effectiveAnkiConfig.enabled !== true) {
|
||||||
input.logger.error('Headless known-word refresh failed: AnkiConnect integration not enabled');
|
input.logger.error('Headless known-word refresh failed: AnkiConnect integration not enabled');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
input.requestAppQuit();
|
input.requestAppQuit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveAnkiConfig =
|
|
||||||
input.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(input.resolvedConfig.ankiConnect) ??
|
|
||||||
input.resolvedConfig.ankiConnect;
|
|
||||||
const integration = new AnkiIntegration(
|
const integration = new AnkiIntegration(
|
||||||
effectiveAnkiConfig,
|
effectiveAnkiConfig,
|
||||||
new SubtitleTimingTracker(),
|
new SubtitleTimingTracker(),
|
||||||
@@ -40,7 +41,7 @@ export async function runHeadlessKnownWordRefresh(input: {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
}),
|
}),
|
||||||
path.join(input.userDataPath, 'known-words-cache.json'),
|
path.join(input.userDataPath, 'known-words-cache.json'),
|
||||||
mergeAiConfig(input.resolvedConfig.ai, input.resolvedConfig.ankiConnect?.ai),
|
mergeAiConfig(input.resolvedConfig.ai, effectiveAnkiConfig.ai),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -129,3 +129,48 @@ test('headless startup runtime accepts grouped app lifecycle input', () => {
|
|||||||
assert.deepEqual(runtime.runAndApplyStartupState(), { mode: 'started' });
|
assert.deepEqual(runtime.runAndApplyStartupState(), { mode: 'started' });
|
||||||
assert.deepEqual(calls, ['lifecycle:start', 'lifecycle:start', 'apply:started']);
|
assert.deepEqual(calls, ['lifecycle:start', 'lifecycle:start', 'apply:started']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createHeadlessStartupRuntime<
|
||||||
|
{ mode: string },
|
||||||
|
{ startAppLifecycle: (args: CliArgs) => void; customFlag: boolean }
|
||||||
|
>(
|
||||||
|
// @ts-expect-error custom bootstrap deps require an explicit factory
|
||||||
|
{
|
||||||
|
appLifecycleRuntimeRunnerMainDeps: {
|
||||||
|
app: { on: () => {} } as never,
|
||||||
|
platform: 'darwin',
|
||||||
|
shouldStartApp: () => true,
|
||||||
|
parseArgs: () => ({}) as never,
|
||||||
|
handleCliCommand: () => {},
|
||||||
|
printHelp: () => {},
|
||||||
|
logNoRunningInstance: () => {},
|
||||||
|
onReady: async () => {},
|
||||||
|
onWillQuitCleanup: () => {},
|
||||||
|
shouldRestoreWindowsOnActivate: () => false,
|
||||||
|
restoreWindowsOnActivate: () => {},
|
||||||
|
shouldQuitOnWindowAllClosed: () => false,
|
||||||
|
},
|
||||||
|
bootstrap: {
|
||||||
|
argv: ['node', 'main.js'],
|
||||||
|
parseArgs: () => ({ command: 'start' }) as never,
|
||||||
|
setLogLevel: (_level: string, _source: LogLevelSource) => {},
|
||||||
|
forceX11Backend: () => {},
|
||||||
|
enforceUnsupportedWaylandMode: () => {},
|
||||||
|
shouldStartApp: () => true,
|
||||||
|
getDefaultSocketPath: () => '/tmp/mpv.sock',
|
||||||
|
defaultTexthookerPort: 5174,
|
||||||
|
configDir: '/tmp/config',
|
||||||
|
defaultConfig: {} as never,
|
||||||
|
generateConfigTemplate: () => 'template',
|
||||||
|
generateDefaultConfigFile: async () => 0,
|
||||||
|
setExitCode: () => {},
|
||||||
|
quitApp: () => {},
|
||||||
|
logGenerateConfigError: () => {},
|
||||||
|
startAppLifecycle: () => {},
|
||||||
|
},
|
||||||
|
runStartupBootstrapRuntime: (deps) => {
|
||||||
|
assert.equal(deps.customFlag, true);
|
||||||
|
return { mode: 'started' };
|
||||||
|
},
|
||||||
|
applyStartupState: () => {},
|
||||||
|
});
|
||||||
|
|||||||
@@ -39,34 +39,48 @@ export interface HeadlessStartupBootstrapInput {
|
|||||||
|
|
||||||
export type HeadlessStartupAppLifecycleInput = AppLifecycleRuntimeRunnerParams;
|
export type HeadlessStartupAppLifecycleInput = AppLifecycleRuntimeRunnerParams;
|
||||||
|
|
||||||
export interface HeadlessStartupRuntimeInput<
|
interface HeadlessStartupRuntimeSharedInput<TStartupState> {
|
||||||
TStartupState,
|
|
||||||
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
|
||||||
> {
|
|
||||||
appLifecycleRuntimeRunnerMainDeps?: AppLifecycleDepsRuntimeOptions;
|
appLifecycleRuntimeRunnerMainDeps?: AppLifecycleDepsRuntimeOptions;
|
||||||
appLifecycle?: HeadlessStartupAppLifecycleInput;
|
appLifecycle?: HeadlessStartupAppLifecycleInput;
|
||||||
bootstrap: HeadlessStartupBootstrapInput;
|
bootstrap: HeadlessStartupBootstrapInput;
|
||||||
createAppLifecycleRuntimeRunner?: (
|
createAppLifecycleRuntimeRunner?: (
|
||||||
params: AppLifecycleDepsRuntimeOptions,
|
params: AppLifecycleDepsRuntimeOptions,
|
||||||
) => (args: CliArgs) => void;
|
) => (args: CliArgs) => void;
|
||||||
createStartupBootstrapRuntimeDeps?: (
|
applyStartupState: (startupState: TStartupState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeadlessStartupRuntimeDefaultInput<TStartupState>
|
||||||
|
extends HeadlessStartupRuntimeSharedInput<TStartupState> {
|
||||||
|
createStartupBootstrapRuntimeDeps?: undefined;
|
||||||
|
runStartupBootstrapRuntime: (deps: StartupBootstrapRuntimeDeps) => TStartupState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeadlessStartupRuntimeCustomInput<TStartupState, TStartupBootstrapRuntimeDeps>
|
||||||
|
extends HeadlessStartupRuntimeSharedInput<TStartupState> {
|
||||||
|
createStartupBootstrapRuntimeDeps: (
|
||||||
deps: StartupBootstrapRuntimeFactoryDeps,
|
deps: StartupBootstrapRuntimeFactoryDeps,
|
||||||
) => TStartupBootstrapRuntimeDeps;
|
) => TStartupBootstrapRuntimeDeps;
|
||||||
runStartupBootstrapRuntime: (deps: TStartupBootstrapRuntimeDeps) => TStartupState;
|
runStartupBootstrapRuntime: (deps: TStartupBootstrapRuntimeDeps) => TStartupState;
|
||||||
applyStartupState: (startupState: TStartupState) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HeadlessStartupRuntimeInput<
|
||||||
|
TStartupState,
|
||||||
|
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
||||||
|
> =
|
||||||
|
| HeadlessStartupRuntimeDefaultInput<TStartupState>
|
||||||
|
| HeadlessStartupRuntimeCustomInput<TStartupState, TStartupBootstrapRuntimeDeps>;
|
||||||
|
|
||||||
export interface HeadlessStartupRuntime<TStartupState> {
|
export interface HeadlessStartupRuntime<TStartupState> {
|
||||||
appLifecycleRuntimeRunner: (args: CliArgs) => void;
|
appLifecycleRuntimeRunner: (args: CliArgs) => void;
|
||||||
runAndApplyStartupState: () => TStartupState;
|
runAndApplyStartupState: () => TStartupState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHeadlessStartupRuntime<
|
function resolveAppLifecycleRuntimeRunnerMainDeps(
|
||||||
TStartupState,
|
input: Pick<
|
||||||
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
HeadlessStartupRuntimeSharedInput<unknown>,
|
||||||
>(
|
'appLifecycleRuntimeRunnerMainDeps' | 'appLifecycle'
|
||||||
input: HeadlessStartupRuntimeInput<TStartupState, TStartupBootstrapRuntimeDeps>,
|
>,
|
||||||
): HeadlessStartupRuntime<TStartupState> {
|
) {
|
||||||
const appLifecycleRuntimeRunnerMainDeps =
|
const appLifecycleRuntimeRunnerMainDeps =
|
||||||
input.appLifecycleRuntimeRunnerMainDeps ?? input.appLifecycle;
|
input.appLifecycleRuntimeRunnerMainDeps ?? input.appLifecycle;
|
||||||
|
|
||||||
@@ -74,30 +88,57 @@ export function createHeadlessStartupRuntime<
|
|||||||
throw new Error('Headless startup runtime needs app lifecycle runtime runner deps');
|
throw new Error('Headless startup runtime needs app lifecycle runtime runner deps');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { appLifecycleRuntimeRunner, runAndApplyStartupState } = composeHeadlessStartupHandlers({
|
return createBuildAppLifecycleRuntimeRunnerMainDepsHandler(appLifecycleRuntimeRunnerMainDeps)();
|
||||||
startupRuntimeHandlersDeps: {
|
}
|
||||||
appLifecycleRuntimeRunnerMainDeps: createBuildAppLifecycleRuntimeRunnerMainDepsHandler(
|
|
||||||
appLifecycleRuntimeRunnerMainDeps,
|
function buildHeadlessStartupHandlersDeps<TStartupState>(
|
||||||
)(),
|
input: HeadlessStartupRuntimeSharedInput<TStartupState>,
|
||||||
createAppLifecycleRuntimeRunner:
|
) {
|
||||||
input.createAppLifecycleRuntimeRunner ??
|
const appLifecycleRuntimeRunnerMainDeps =
|
||||||
((params: AppLifecycleDepsRuntimeOptions) => (args: CliArgs) =>
|
resolveAppLifecycleRuntimeRunnerMainDeps(input);
|
||||||
startAppLifecycle(
|
|
||||||
args,
|
return {
|
||||||
createAppLifecycleDepsRuntime(createAppLifecycleRuntimeDeps(params)),
|
appLifecycleRuntimeRunnerMainDeps,
|
||||||
)),
|
createAppLifecycleRuntimeRunner:
|
||||||
buildStartupBootstrapMainDeps: (startAppLifecycle: (args: CliArgs) => void) => ({
|
input.createAppLifecycleRuntimeRunner ??
|
||||||
...input.bootstrap,
|
((params: AppLifecycleDepsRuntimeOptions) => (args: CliArgs) =>
|
||||||
startAppLifecycle,
|
startAppLifecycle(args, createAppLifecycleDepsRuntime(createAppLifecycleRuntimeDeps(params)))),
|
||||||
}),
|
buildStartupBootstrapMainDeps: (startAppLifecycle: (args: CliArgs) => void) => ({
|
||||||
createStartupBootstrapRuntimeDeps: (deps: StartupBootstrapRuntimeFactoryDeps) =>
|
...input.bootstrap,
|
||||||
input.createStartupBootstrapRuntimeDeps
|
startAppLifecycle,
|
||||||
? input.createStartupBootstrapRuntimeDeps(deps)
|
}),
|
||||||
: (createStartupBootstrapRuntimeDeps(deps) as unknown as TStartupBootstrapRuntimeDeps),
|
applyStartupState: input.applyStartupState,
|
||||||
runStartupBootstrapRuntime: input.runStartupBootstrapRuntime,
|
};
|
||||||
applyStartupState: input.applyStartupState,
|
}
|
||||||
},
|
|
||||||
});
|
export function createHeadlessStartupRuntime<TStartupState>(
|
||||||
|
input: HeadlessStartupRuntimeDefaultInput<TStartupState>,
|
||||||
|
): HeadlessStartupRuntime<TStartupState>;
|
||||||
|
export function createHeadlessStartupRuntime<TStartupState, TStartupBootstrapRuntimeDeps>(
|
||||||
|
input: HeadlessStartupRuntimeCustomInput<TStartupState, TStartupBootstrapRuntimeDeps>,
|
||||||
|
): HeadlessStartupRuntime<TStartupState>;
|
||||||
|
export function createHeadlessStartupRuntime<TStartupState, TStartupBootstrapRuntimeDeps>(
|
||||||
|
input: HeadlessStartupRuntimeInput<TStartupState, TStartupBootstrapRuntimeDeps>,
|
||||||
|
): HeadlessStartupRuntime<TStartupState> {
|
||||||
|
const baseDeps = buildHeadlessStartupHandlersDeps(input);
|
||||||
|
const { appLifecycleRuntimeRunner, runAndApplyStartupState } =
|
||||||
|
'createStartupBootstrapRuntimeDeps' in input && input.createStartupBootstrapRuntimeDeps
|
||||||
|
? composeHeadlessStartupHandlers<CliArgs, TStartupState, TStartupBootstrapRuntimeDeps>({
|
||||||
|
startupRuntimeHandlersDeps: {
|
||||||
|
...baseDeps,
|
||||||
|
createStartupBootstrapRuntimeDeps: (deps: StartupBootstrapRuntimeFactoryDeps) =>
|
||||||
|
input.createStartupBootstrapRuntimeDeps(deps),
|
||||||
|
runStartupBootstrapRuntime: input.runStartupBootstrapRuntime,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: composeHeadlessStartupHandlers<CliArgs, TStartupState, StartupBootstrapRuntimeDeps>({
|
||||||
|
startupRuntimeHandlersDeps: {
|
||||||
|
...baseDeps,
|
||||||
|
createStartupBootstrapRuntimeDeps: (deps: StartupBootstrapRuntimeFactoryDeps) =>
|
||||||
|
createStartupBootstrapRuntimeDeps(deps),
|
||||||
|
runStartupBootstrapRuntime: input.runStartupBootstrapRuntime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appLifecycleRuntimeRunner,
|
appLifecycleRuntimeRunner,
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export function createMainStartupBootstrap<TStartupState>(
|
|||||||
defaultConfig: input.config.defaultConfig,
|
defaultConfig: input.config.defaultConfig,
|
||||||
getResolvedConfig: () => input.config.configService.getConfig(),
|
getResolvedConfig: () => input.config.configService.getConfig(),
|
||||||
setCliLogLevel: (level) => input.logging.setLogLevel(level, 'cli'),
|
setCliLogLevel: (level) => input.logging.setLogLevel(level, 'cli'),
|
||||||
hasMpvWebsocketPlugin: () => true,
|
hasMpvWebsocketPlugin: () => input.commands.hasMpvWebsocketPlugin(),
|
||||||
},
|
},
|
||||||
io: {
|
io: {
|
||||||
texthookerService: input.runtime.texthookerService,
|
texthookerService: input.runtime.texthookerService,
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ export function createMainStartupRuntime<TStartupState>(
|
|||||||
): MainStartupRuntime<TStartupState> {
|
): MainStartupRuntime<TStartupState> {
|
||||||
const appReady = createAppReadyRuntime(input.appReady);
|
const appReady = createAppReadyRuntime(input.appReady);
|
||||||
const cliStartup = createCliStartupRuntime(input.cli);
|
const cliStartup = createCliStartupRuntime(input.cli);
|
||||||
const headlessStartup = createHeadlessStartupRuntime<TStartupState>(input.headless);
|
const headlessStartup =
|
||||||
|
'createStartupBootstrapRuntimeDeps' in input.headless &&
|
||||||
|
input.headless.createStartupBootstrapRuntimeDeps
|
||||||
|
? createHeadlessStartupRuntime(input.headless)
|
||||||
|
: createHeadlessStartupRuntime(input.headless);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appReady,
|
appReady,
|
||||||
|
|||||||
@@ -260,15 +260,7 @@ export function createMpvRuntimeFromMainState(
|
|||||||
handleMpvConnectionChange: (connected) => {
|
handleMpvConnectionChange: (connected) => {
|
||||||
input.youtube.handleMpvConnectionChange(connected);
|
input.youtube.handleMpvConnectionChange(connected);
|
||||||
},
|
},
|
||||||
handleMediaPathChange: (path) => {
|
handleMediaPathChange: (path) => input.youtube.handleMediaPathChange(path),
|
||||||
input.youtube.invalidatePendingAutoplayReadyFallbacks();
|
|
||||||
input.currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
|
||||||
input.startupOsdSequencer.reset();
|
|
||||||
input.youtube.handleMediaPathChange(path);
|
|
||||||
if (path) {
|
|
||||||
input.stats.ensureImmersionTrackerStarted();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleSubtitleTrackChange: (sid) => {
|
handleSubtitleTrackChange: (sid) => {
|
||||||
input.youtube.handleSubtitleTrackChange(sid);
|
input.youtube.handleSubtitleTrackChange(sid);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -308,6 +308,153 @@ test('overlay ui runtime initializes overlay runtime before visible action when
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('overlay ui runtime initializes overlay runtime before overlay visibility action when needed', async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
let overlayRuntimeInitialized = false;
|
||||||
|
|
||||||
|
const overlayUi = createOverlayUiRuntime({
|
||||||
|
windowState: {
|
||||||
|
getMainWindow: () => null,
|
||||||
|
setMainWindow: () => {},
|
||||||
|
getModalWindow: () => null,
|
||||||
|
setModalWindow: () => {},
|
||||||
|
getVisibleOverlayVisible: () => false,
|
||||||
|
setVisibleOverlayVisible: () => {},
|
||||||
|
getOverlayDebugVisualizationEnabled: () => false,
|
||||||
|
setOverlayDebugVisualizationEnabled: () => {},
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
getCurrentOverlayGeometry: () => ({ x: 0, y: 0, width: 100, height: 100 }),
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
onModalStateChange: () => {},
|
||||||
|
},
|
||||||
|
modalRuntime: {
|
||||||
|
handleOverlayModalClosed: () => {},
|
||||||
|
notifyOverlayModalOpened: () => {},
|
||||||
|
waitForModalOpen: async () => false,
|
||||||
|
getRestoreVisibleOverlayOnModalClose: () => new Set<OverlayHostedModal>(),
|
||||||
|
openRuntimeOptionsPalette: () => {},
|
||||||
|
sendToActiveOverlayWindow: () => false,
|
||||||
|
},
|
||||||
|
visibilityService: {
|
||||||
|
getModalActive: () => false,
|
||||||
|
getForceMousePassthrough: () => false,
|
||||||
|
getWindowTracker: () => null,
|
||||||
|
getTrackerNotReadyWarningShown: () => false,
|
||||||
|
setTrackerNotReadyWarningShown: () => {},
|
||||||
|
updateVisibleOverlayBounds: () => {},
|
||||||
|
ensureOverlayWindowLevel: () => {},
|
||||||
|
syncPrimaryOverlayWindowLayer: () => {},
|
||||||
|
enforceOverlayLayerOrder: () => {},
|
||||||
|
syncOverlayShortcuts: () => {},
|
||||||
|
isMacOSPlatform: () => false,
|
||||||
|
isWindowsPlatform: () => false,
|
||||||
|
showOverlayLoadingOsd: () => {},
|
||||||
|
resolveFallbackBounds: () => ({ x: 0, y: 0, width: 100, height: 100 }),
|
||||||
|
},
|
||||||
|
overlayWindows: {
|
||||||
|
createOverlayWindowCore: () => createWindow(),
|
||||||
|
isDev: false,
|
||||||
|
ensureOverlayWindowLevel: () => {},
|
||||||
|
onRuntimeOptionsChanged: () => {},
|
||||||
|
setOverlayDebugVisualizationEnabled: () => {},
|
||||||
|
isOverlayVisible: () => false,
|
||||||
|
getYomitanSession: () => null,
|
||||||
|
tryHandleOverlayShortcutLocalFallback: () => false,
|
||||||
|
forwardTabToMpv: () => {},
|
||||||
|
onWindowClosed: () => {},
|
||||||
|
},
|
||||||
|
visibilityActions: {
|
||||||
|
setVisibleOverlayVisibleCore: ({ visible }) => {
|
||||||
|
calls.push(`setVisible:${visible}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overlayActions: {
|
||||||
|
getRuntimeOptionsManager: () => null,
|
||||||
|
getMpvClient: () => null,
|
||||||
|
broadcastRuntimeOptionsChangedRuntime: () => {},
|
||||||
|
broadcastToOverlayWindows: () => {},
|
||||||
|
setOverlayDebugVisualizationEnabledRuntime: () => {},
|
||||||
|
},
|
||||||
|
tray: null,
|
||||||
|
bootstrap: {
|
||||||
|
initializeOverlayRuntimeMainDeps: {
|
||||||
|
appState: {
|
||||||
|
backendOverride: null,
|
||||||
|
windowTracker: null,
|
||||||
|
subtitleTimingTracker: null,
|
||||||
|
mpvClient: null,
|
||||||
|
mpvSocketPath: '/tmp/mpv.sock',
|
||||||
|
runtimeOptionsManager: null,
|
||||||
|
ankiIntegration: null,
|
||||||
|
},
|
||||||
|
overlayManager: {
|
||||||
|
getVisibleOverlayVisible: () => false,
|
||||||
|
},
|
||||||
|
overlayVisibilityRuntime: {
|
||||||
|
updateVisibleOverlayVisibility: () => {},
|
||||||
|
},
|
||||||
|
overlayShortcutsRuntime: {
|
||||||
|
syncOverlayShortcuts: () => {},
|
||||||
|
},
|
||||||
|
createMainWindow: () => {},
|
||||||
|
registerGlobalShortcuts: () => {},
|
||||||
|
updateVisibleOverlayBounds: () => {},
|
||||||
|
getOverlayWindows: () => [],
|
||||||
|
getResolvedConfig: () => ({ ankiConnect: {} }) as never,
|
||||||
|
showDesktopNotification: () => {},
|
||||||
|
createFieldGroupingCallback: () => () => Promise.resolve({} as never),
|
||||||
|
getKnownWordCacheStatePath: () => '/tmp/known.json',
|
||||||
|
shouldStartAnkiIntegration: () => false,
|
||||||
|
},
|
||||||
|
initializeOverlayRuntimeBootstrapDeps: {
|
||||||
|
isOverlayRuntimeInitialized: () => overlayRuntimeInitialized,
|
||||||
|
initializeOverlayRuntimeCore: () => {
|
||||||
|
calls.push('initializeOverlayRuntimeCore');
|
||||||
|
},
|
||||||
|
setOverlayRuntimeInitialized: (initialized) => {
|
||||||
|
overlayRuntimeInitialized = initialized;
|
||||||
|
calls.push(`setInitialized:${initialized}`);
|
||||||
|
},
|
||||||
|
startBackgroundWarmups: () => {
|
||||||
|
calls.push('startBackgroundWarmups');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onInitialized: () => {
|
||||||
|
calls.push('onInitialized');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtimeState: {
|
||||||
|
isOverlayRuntimeInitialized: () => overlayRuntimeInitialized,
|
||||||
|
setOverlayRuntimeInitialized: (initialized) => {
|
||||||
|
overlayRuntimeInitialized = initialized;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mpvSubtitle: {
|
||||||
|
ensureOverlayMpvSubtitlesHidden: async () => {
|
||||||
|
calls.push('hideMpvSubs');
|
||||||
|
},
|
||||||
|
syncOverlayMpvSubtitleSuppression: () => {
|
||||||
|
calls.push('syncMpvSubs');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
overlayUi.setOverlayVisible(true);
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'setInitialized:true',
|
||||||
|
'initializeOverlayRuntimeCore',
|
||||||
|
'startBackgroundWarmups',
|
||||||
|
'onInitialized',
|
||||||
|
'syncMpvSubs',
|
||||||
|
'hideMpvSubs',
|
||||||
|
'setVisible:true',
|
||||||
|
'syncMpvSubs',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('overlay ui runtime delegates modal actions to injected modal runtime', async () => {
|
test('overlay ui runtime delegates modal actions to injected modal runtime', async () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const restoreOnClose = new Set<OverlayHostedModal>();
|
const restoreOnClose = new Set<OverlayHostedModal>();
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ export function createOverlayUiRuntime<TWindow extends WindowLike>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setOverlayVisible(visible: boolean): void {
|
function setOverlayVisible(visible: boolean): void {
|
||||||
|
ensureOverlayWindowsReadyForVisibilityActions();
|
||||||
if (visible) {
|
if (visible) {
|
||||||
void runtimeInput.mpvSubtitle.ensureOverlayMpvSubtitlesHidden();
|
void runtimeInput.mpvSubtitle.ensureOverlayMpvSubtitlesHidden();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createDiscordPresenceService } from '../../core/services';
|
import { createDiscordPresenceService } from '../../core/services/discord-presence';
|
||||||
import type { ResolvedConfig } from '../../types';
|
import type { ResolvedConfig } from '../../types';
|
||||||
import { createDiscordRpcClient } from './discord-rpc-client.js';
|
import { createDiscordRpcClient } from './discord-rpc-client.js';
|
||||||
|
|
||||||
@@ -95,6 +95,10 @@ export function createDiscordPresenceRuntimeFromMainState(input: {
|
|||||||
}) {
|
}) {
|
||||||
const sessionStartedAtMs = Date.now();
|
const sessionStartedAtMs = Date.now();
|
||||||
let mediaDurationSec: number | null = null;
|
let mediaDurationSec: number | null = null;
|
||||||
|
const stopCurrentDiscordPresenceService = async (): Promise<void> => {
|
||||||
|
await input.appState.discordPresenceService?.stop?.();
|
||||||
|
input.appState.discordPresenceService = null;
|
||||||
|
};
|
||||||
|
|
||||||
const discordPresenceRuntime = createDiscordPresenceRuntime({
|
const discordPresenceRuntime = createDiscordPresenceRuntime({
|
||||||
getDiscordPresenceService: () => input.appState.discordPresenceService,
|
getDiscordPresenceService: () => input.appState.discordPresenceService,
|
||||||
@@ -114,10 +118,11 @@ export function createDiscordPresenceRuntimeFromMainState(input: {
|
|||||||
|
|
||||||
const initializeDiscordPresenceService = async (): Promise<void> => {
|
const initializeDiscordPresenceService = async (): Promise<void> => {
|
||||||
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
||||||
input.appState.discordPresenceService = null;
|
await stopCurrentDiscordPresenceService();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await stopCurrentDiscordPresenceService();
|
||||||
input.appState.discordPresenceService = createDiscordPresenceService({
|
input.appState.discordPresenceService = createDiscordPresenceService({
|
||||||
config: input.getResolvedConfig().discordPresence,
|
config: input.getResolvedConfig().discordPresence,
|
||||||
createClient: () => createDiscordRpcClient(input.appId),
|
createClient: () => createDiscordRpcClient(input.appId),
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ export function createStatsRuntimeCoordinator(
|
|||||||
await syncYomitanDefaultAnkiServerCore(ankiUrl, yomitanDeps, yomitanLogger, {
|
await syncYomitanDefaultAnkiServerCore(ankiUrl, yomitanDeps, yomitanLogger, {
|
||||||
forceOverride: true,
|
forceOverride: true,
|
||||||
});
|
});
|
||||||
return addYomitanNoteViaSearch(word, yomitanDeps, yomitanLogger);
|
const result = await addYomitanNoteViaSearch(word, yomitanDeps, yomitanLogger);
|
||||||
|
return result.noteId;
|
||||||
},
|
},
|
||||||
openExternal: input.actions.openExternal,
|
openExternal: input.actions.openExternal,
|
||||||
requestAppQuit: input.actions.requestAppQuit,
|
requestAppQuit: input.actions.requestAppQuit,
|
||||||
|
|||||||
@@ -129,3 +129,44 @@ test('stats runtime stops owned server and clears daemon state during quit clean
|
|||||||
assert.equal(runtime.getStatsServer(), null);
|
assert.equal(runtime.getStatsServer(), null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stats runtime stops the in-process background server without signalling the current process', async () => {
|
||||||
|
await withTempDir(async (dir) => {
|
||||||
|
const statePath = path.join(dir, 'stats-daemon.json');
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const runtime = createStatsRuntime({
|
||||||
|
statsDaemonStatePath: statePath,
|
||||||
|
getResolvedConfig: () => ({
|
||||||
|
immersionTracking: { enabled: true },
|
||||||
|
stats: { serverPort: 6972 },
|
||||||
|
}),
|
||||||
|
getImmersionTracker: () => ({}) as never,
|
||||||
|
ensureImmersionTrackerStartedCore: () => {},
|
||||||
|
startStatsServer: () => ({
|
||||||
|
close: () => {
|
||||||
|
calls.push('close');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
openExternal: async () => {},
|
||||||
|
exitAppWithCode: () => {},
|
||||||
|
logInfo: () => {},
|
||||||
|
logWarn: () => {},
|
||||||
|
logError: () => {},
|
||||||
|
getCurrentPid: () => 321,
|
||||||
|
isProcessAlive: () => true,
|
||||||
|
killProcess: () => {
|
||||||
|
calls.push('kill');
|
||||||
|
},
|
||||||
|
now: () => 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.ensureBackgroundStatsServerStarted();
|
||||||
|
const result = await runtime.stopBackgroundStatsServer();
|
||||||
|
|
||||||
|
assert.deepEqual(result, { ok: true, stale: false });
|
||||||
|
assert.deepEqual(calls, ['close']);
|
||||||
|
assert.equal(fs.existsSync(statePath), false);
|
||||||
|
assert.equal(runtime.getStatsServer(), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -270,6 +270,15 @@ export function createStatsRuntime<
|
|||||||
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
||||||
return { ok: true, stale: true };
|
return { ok: true, stale: true };
|
||||||
}
|
}
|
||||||
|
if (state.pid === getCurrentPid()) {
|
||||||
|
if (!statsServer) {
|
||||||
|
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
||||||
|
return { ok: true, stale: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
stopStatsServer();
|
||||||
|
return { ok: true, stale: false };
|
||||||
|
}
|
||||||
if (!isProcessAlive(state.pid)) {
|
if (!isProcessAlive(state.pid)) {
|
||||||
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
||||||
return { ok: true, stale: true };
|
return { ok: true, stale: true };
|
||||||
|
|||||||
Reference in New Issue
Block a user