mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-03 18:12:07 -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);
|
||||
input.appState.anilistSetupWindow = window;
|
||||
window.on('closed', () => {
|
||||
input.appState.anilistSetupWindow = null;
|
||||
if (input.appState.anilistSetupWindow === window) {
|
||||
input.appState.anilistSetupWindow = null;
|
||||
}
|
||||
});
|
||||
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']);
|
||||
});
|
||||
|
||||
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 {
|
||||
let discordPresenceMediaDurationSec: number | null = null;
|
||||
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)({
|
||||
getDiscordPresenceService: () => input.getDiscordPresenceService(),
|
||||
@@ -72,19 +76,17 @@ export function createDiscordPresenceLifecycleRuntime(
|
||||
},
|
||||
initializeDiscordPresenceService: async () => {
|
||||
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
||||
input.setDiscordPresenceService(null);
|
||||
await stopCurrentDiscordPresenceService();
|
||||
return;
|
||||
}
|
||||
|
||||
await stopCurrentDiscordPresenceService();
|
||||
input.setDiscordPresenceService(
|
||||
input.createDiscordPresenceService(input.getResolvedConfig().discordPresence),
|
||||
);
|
||||
await input.getDiscordPresenceService()?.start();
|
||||
discordPresenceRuntime.publishDiscordPresence();
|
||||
},
|
||||
stopDiscordPresenceService: async () => {
|
||||
await input.getDiscordPresenceService()?.stop?.();
|
||||
input.setDiscordPresenceService(null);
|
||||
},
|
||||
stopDiscordPresenceService: stopCurrentDiscordPresenceService,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,16 +17,17 @@ export async function runHeadlessKnownWordRefresh(input: {
|
||||
};
|
||||
requestAppQuit: () => 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');
|
||||
process.exitCode = 1;
|
||||
input.requestAppQuit();
|
||||
return;
|
||||
}
|
||||
|
||||
const effectiveAnkiConfig =
|
||||
input.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(input.resolvedConfig.ankiConnect) ??
|
||||
input.resolvedConfig.ankiConnect;
|
||||
const integration = new AnkiIntegration(
|
||||
effectiveAnkiConfig,
|
||||
new SubtitleTimingTracker(),
|
||||
@@ -40,7 +41,7 @@ export async function runHeadlessKnownWordRefresh(input: {
|
||||
cancelled: true,
|
||||
}),
|
||||
path.join(input.userDataPath, 'known-words-cache.json'),
|
||||
mergeAiConfig(input.resolvedConfig.ai, input.resolvedConfig.ankiConnect?.ai),
|
||||
mergeAiConfig(input.resolvedConfig.ai, effectiveAnkiConfig.ai),
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@@ -129,3 +129,48 @@ test('headless startup runtime accepts grouped app lifecycle input', () => {
|
||||
assert.deepEqual(runtime.runAndApplyStartupState(), { mode: '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 interface HeadlessStartupRuntimeInput<
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
||||
> {
|
||||
interface HeadlessStartupRuntimeSharedInput<TStartupState> {
|
||||
appLifecycleRuntimeRunnerMainDeps?: AppLifecycleDepsRuntimeOptions;
|
||||
appLifecycle?: HeadlessStartupAppLifecycleInput;
|
||||
bootstrap: HeadlessStartupBootstrapInput;
|
||||
createAppLifecycleRuntimeRunner?: (
|
||||
params: AppLifecycleDepsRuntimeOptions,
|
||||
) => (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,
|
||||
) => TStartupBootstrapRuntimeDeps;
|
||||
runStartupBootstrapRuntime: (deps: TStartupBootstrapRuntimeDeps) => TStartupState;
|
||||
applyStartupState: (startupState: TStartupState) => void;
|
||||
}
|
||||
|
||||
export type HeadlessStartupRuntimeInput<
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
||||
> =
|
||||
| HeadlessStartupRuntimeDefaultInput<TStartupState>
|
||||
| HeadlessStartupRuntimeCustomInput<TStartupState, TStartupBootstrapRuntimeDeps>;
|
||||
|
||||
export interface HeadlessStartupRuntime<TStartupState> {
|
||||
appLifecycleRuntimeRunner: (args: CliArgs) => void;
|
||||
runAndApplyStartupState: () => TStartupState;
|
||||
}
|
||||
|
||||
export function createHeadlessStartupRuntime<
|
||||
TStartupState,
|
||||
TStartupBootstrapRuntimeDeps = StartupBootstrapRuntimeDeps,
|
||||
>(
|
||||
input: HeadlessStartupRuntimeInput<TStartupState, TStartupBootstrapRuntimeDeps>,
|
||||
): HeadlessStartupRuntime<TStartupState> {
|
||||
function resolveAppLifecycleRuntimeRunnerMainDeps(
|
||||
input: Pick<
|
||||
HeadlessStartupRuntimeSharedInput<unknown>,
|
||||
'appLifecycleRuntimeRunnerMainDeps' | 'appLifecycle'
|
||||
>,
|
||||
) {
|
||||
const appLifecycleRuntimeRunnerMainDeps =
|
||||
input.appLifecycleRuntimeRunnerMainDeps ?? input.appLifecycle;
|
||||
|
||||
@@ -74,30 +88,57 @@ export function createHeadlessStartupRuntime<
|
||||
throw new Error('Headless startup runtime needs app lifecycle runtime runner deps');
|
||||
}
|
||||
|
||||
const { appLifecycleRuntimeRunner, runAndApplyStartupState } = composeHeadlessStartupHandlers({
|
||||
startupRuntimeHandlersDeps: {
|
||||
appLifecycleRuntimeRunnerMainDeps: createBuildAppLifecycleRuntimeRunnerMainDepsHandler(
|
||||
appLifecycleRuntimeRunnerMainDeps,
|
||||
)(),
|
||||
createAppLifecycleRuntimeRunner:
|
||||
input.createAppLifecycleRuntimeRunner ??
|
||||
((params: AppLifecycleDepsRuntimeOptions) => (args: CliArgs) =>
|
||||
startAppLifecycle(
|
||||
args,
|
||||
createAppLifecycleDepsRuntime(createAppLifecycleRuntimeDeps(params)),
|
||||
)),
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle: (args: CliArgs) => void) => ({
|
||||
...input.bootstrap,
|
||||
startAppLifecycle,
|
||||
}),
|
||||
createStartupBootstrapRuntimeDeps: (deps: StartupBootstrapRuntimeFactoryDeps) =>
|
||||
input.createStartupBootstrapRuntimeDeps
|
||||
? input.createStartupBootstrapRuntimeDeps(deps)
|
||||
: (createStartupBootstrapRuntimeDeps(deps) as unknown as TStartupBootstrapRuntimeDeps),
|
||||
runStartupBootstrapRuntime: input.runStartupBootstrapRuntime,
|
||||
applyStartupState: input.applyStartupState,
|
||||
},
|
||||
});
|
||||
return createBuildAppLifecycleRuntimeRunnerMainDepsHandler(appLifecycleRuntimeRunnerMainDeps)();
|
||||
}
|
||||
|
||||
function buildHeadlessStartupHandlersDeps<TStartupState>(
|
||||
input: HeadlessStartupRuntimeSharedInput<TStartupState>,
|
||||
) {
|
||||
const appLifecycleRuntimeRunnerMainDeps =
|
||||
resolveAppLifecycleRuntimeRunnerMainDeps(input);
|
||||
|
||||
return {
|
||||
appLifecycleRuntimeRunnerMainDeps,
|
||||
createAppLifecycleRuntimeRunner:
|
||||
input.createAppLifecycleRuntimeRunner ??
|
||||
((params: AppLifecycleDepsRuntimeOptions) => (args: CliArgs) =>
|
||||
startAppLifecycle(args, createAppLifecycleDepsRuntime(createAppLifecycleRuntimeDeps(params)))),
|
||||
buildStartupBootstrapMainDeps: (startAppLifecycle: (args: CliArgs) => void) => ({
|
||||
...input.bootstrap,
|
||||
startAppLifecycle,
|
||||
}),
|
||||
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 {
|
||||
appLifecycleRuntimeRunner,
|
||||
|
||||
@@ -393,7 +393,7 @@ export function createMainStartupBootstrap<TStartupState>(
|
||||
defaultConfig: input.config.defaultConfig,
|
||||
getResolvedConfig: () => input.config.configService.getConfig(),
|
||||
setCliLogLevel: (level) => input.logging.setLogLevel(level, 'cli'),
|
||||
hasMpvWebsocketPlugin: () => true,
|
||||
hasMpvWebsocketPlugin: () => input.commands.hasMpvWebsocketPlugin(),
|
||||
},
|
||||
io: {
|
||||
texthookerService: input.runtime.texthookerService,
|
||||
|
||||
@@ -29,7 +29,11 @@ export function createMainStartupRuntime<TStartupState>(
|
||||
): MainStartupRuntime<TStartupState> {
|
||||
const appReady = createAppReadyRuntime(input.appReady);
|
||||
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 {
|
||||
appReady,
|
||||
|
||||
@@ -260,15 +260,7 @@ export function createMpvRuntimeFromMainState(
|
||||
handleMpvConnectionChange: (connected) => {
|
||||
input.youtube.handleMpvConnectionChange(connected);
|
||||
},
|
||||
handleMediaPathChange: (path) => {
|
||||
input.youtube.invalidatePendingAutoplayReadyFallbacks();
|
||||
input.currentMediaTokenizationGate.updateCurrentMediaPath(path);
|
||||
input.startupOsdSequencer.reset();
|
||||
input.youtube.handleMediaPathChange(path);
|
||||
if (path) {
|
||||
input.stats.ensureImmersionTrackerStarted();
|
||||
}
|
||||
},
|
||||
handleMediaPathChange: (path) => input.youtube.handleMediaPathChange(path),
|
||||
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 () => {
|
||||
const calls: string[] = [];
|
||||
const restoreOnClose = new Set<OverlayHostedModal>();
|
||||
|
||||
@@ -368,6 +368,7 @@ export function createOverlayUiRuntime<TWindow extends WindowLike>(
|
||||
}
|
||||
|
||||
function setOverlayVisible(visible: boolean): void {
|
||||
ensureOverlayWindowsReadyForVisibilityActions();
|
||||
if (visible) {
|
||||
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 { createDiscordRpcClient } from './discord-rpc-client.js';
|
||||
|
||||
@@ -95,6 +95,10 @@ export function createDiscordPresenceRuntimeFromMainState(input: {
|
||||
}) {
|
||||
const sessionStartedAtMs = Date.now();
|
||||
let mediaDurationSec: number | null = null;
|
||||
const stopCurrentDiscordPresenceService = async (): Promise<void> => {
|
||||
await input.appState.discordPresenceService?.stop?.();
|
||||
input.appState.discordPresenceService = null;
|
||||
};
|
||||
|
||||
const discordPresenceRuntime = createDiscordPresenceRuntime({
|
||||
getDiscordPresenceService: () => input.appState.discordPresenceService,
|
||||
@@ -114,10 +118,11 @@ export function createDiscordPresenceRuntimeFromMainState(input: {
|
||||
|
||||
const initializeDiscordPresenceService = async (): Promise<void> => {
|
||||
if (input.getResolvedConfig().discordPresence.enabled !== true) {
|
||||
input.appState.discordPresenceService = null;
|
||||
await stopCurrentDiscordPresenceService();
|
||||
return;
|
||||
}
|
||||
|
||||
await stopCurrentDiscordPresenceService();
|
||||
input.appState.discordPresenceService = createDiscordPresenceService({
|
||||
config: input.getResolvedConfig().discordPresence,
|
||||
createClient: () => createDiscordRpcClient(input.appId),
|
||||
|
||||
@@ -112,7 +112,8 @@ export function createStatsRuntimeCoordinator(
|
||||
await syncYomitanDefaultAnkiServerCore(ankiUrl, yomitanDeps, yomitanLogger, {
|
||||
forceOverride: true,
|
||||
});
|
||||
return addYomitanNoteViaSearch(word, yomitanDeps, yomitanLogger);
|
||||
const result = await addYomitanNoteViaSearch(word, yomitanDeps, yomitanLogger);
|
||||
return result.noteId;
|
||||
},
|
||||
openExternal: input.actions.openExternal,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
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)) {
|
||||
removeBackgroundStatsServerState(input.statsDaemonStatePath);
|
||||
return { ok: true, stale: true };
|
||||
|
||||
Reference in New Issue
Block a user