mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-12 03:13:39 -07:00
refactor(main): extract overlay notifications runtime from main.ts
This commit is contained in:
+29
-157
@@ -109,7 +109,6 @@ import type {
|
|||||||
SubtitleMiningContext,
|
SubtitleMiningContext,
|
||||||
SubtitlePosition,
|
SubtitlePosition,
|
||||||
OverlayNotificationPayload,
|
OverlayNotificationPayload,
|
||||||
OverlayNotificationEventPayload,
|
|
||||||
NotificationType,
|
NotificationType,
|
||||||
WindowGeometry,
|
WindowGeometry,
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -545,16 +544,8 @@ import {
|
|||||||
INSTALL_UPDATE_ACTION_ID,
|
INSTALL_UPDATE_ACTION_ID,
|
||||||
UPDATE_AVAILABLE_NOTIFICATION_ID,
|
UPDATE_AVAILABLE_NOTIFICATION_ID,
|
||||||
} from './main/runtime/update/update-notifications';
|
} from './main/runtime/update/update-notifications';
|
||||||
import { createOverlayLoadingOsdController } from './main/runtime/overlay-loading-osd';
|
import { createOverlayNotificationsRuntime } from './main/runtime/overlay-notifications-runtime';
|
||||||
import { createMaybeStartOverlayLoadingOsdHandler } from './main/runtime/overlay-loading-osd-start';
|
import { type ConfiguredStatusNotificationOptions } from './main/runtime/configured-status-notification';
|
||||||
import { withConfiguredOverlayNotificationPosition } from './main/runtime/overlay-notification-position';
|
|
||||||
import { createOverlayNotificationDelivery } from './main/runtime/overlay-notification-delivery';
|
|
||||||
import {
|
|
||||||
getPlaybackFeedbackNotificationOptions,
|
|
||||||
notifyConfiguredStatus,
|
|
||||||
type ConfiguredStatusNotificationOptions,
|
|
||||||
} from './main/runtime/configured-status-notification';
|
|
||||||
import { resolveOverlayReadinessNotificationType } from './main/runtime/notification-routing';
|
|
||||||
import {
|
import {
|
||||||
runUpdateCliCommand,
|
runUpdateCliCommand,
|
||||||
writeUpdateCliCommandResponse,
|
writeUpdateCliCommandResponse,
|
||||||
@@ -3291,177 +3282,58 @@ function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
|||||||
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleOverlayContentReady(): boolean {
|
const overlayNotificationsRuntime = createOverlayNotificationsRuntime({
|
||||||
const overlayWindow = overlayManager.getMainWindow();
|
getResolvedConfig: () => getResolvedConfig(),
|
||||||
return Boolean(
|
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
||||||
overlayManager.getVisibleOverlayVisible() &&
|
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||||
overlayWindow &&
|
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||||
isOverlayWindowReadyForNotification(overlayWindow),
|
showMpvOsd: (message) => showMpvOsd(message),
|
||||||
);
|
getMpvClient: () => appState.mpvClient,
|
||||||
}
|
getAnkiIntegration: () => appState.ankiIntegration,
|
||||||
|
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||||
function getConfiguredStatusNotificationType(): NotificationType {
|
|
||||||
const configuredType = getResolvedConfig().ankiConnect.behavior.notificationType;
|
|
||||||
return resolveOverlayReadinessNotificationType(configuredType, isVisibleOverlayContentReady());
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOverlayWindowReadyForNotification(window: BrowserWindow): boolean {
|
|
||||||
if (window.isDestroyed() || !isOverlayWindowContentReady(window)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (window.webContents.isLoading()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const currentURL = window.webContents.getURL();
|
|
||||||
return currentURL !== '' && currentURL !== 'about:blank';
|
|
||||||
}
|
|
||||||
|
|
||||||
const overlayNotificationDelivery = createOverlayNotificationDelivery({
|
|
||||||
hasReadyOverlayWindow: () => isVisibleOverlayContentReady(),
|
|
||||||
send: (payload) => {
|
|
||||||
broadcastToOverlayWindows(IPC_CHANNELS.event.overlayNotification, payload);
|
|
||||||
},
|
|
||||||
scheduleFlushRetry: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
||||||
clearFlushRetry: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
||||||
});
|
});
|
||||||
let overlayLoadingOsdController: ReturnType<typeof createOverlayLoadingOsdController> | null = null;
|
const {
|
||||||
|
flushQueuedOverlayNotifications,
|
||||||
|
openAnkiCardFromNotification,
|
||||||
|
toggleNotificationHistoryPanel,
|
||||||
|
showConfiguredPlaybackFeedback,
|
||||||
|
maybeStartOverlayLoadingOsd,
|
||||||
|
} = overlayNotificationsRuntime;
|
||||||
|
|
||||||
function flushQueuedOverlayNotifications(): void {
|
// Hoisted wrappers: these names are referenced (directly or via deps object
|
||||||
overlayNotificationDelivery.flush();
|
// literals) during module initialization before this point, so they must stay
|
||||||
}
|
// hoisted function declarations that delegate to the runtime lazily.
|
||||||
|
function getConfiguredStatusNotificationType(): NotificationType {
|
||||||
function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void {
|
return overlayNotificationsRuntime.getConfiguredStatusNotificationType();
|
||||||
overlayNotificationDelivery.send(payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlayNotification(payload: OverlayNotificationPayload): void {
|
function showOverlayNotification(payload: OverlayNotificationPayload): void {
|
||||||
sendOverlayNotificationEvent(
|
overlayNotificationsRuntime.showOverlayNotification(payload);
|
||||||
withConfiguredOverlayNotificationPosition(payload, getResolvedConfig()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissOverlayNotification(id: string): void {
|
|
||||||
sendOverlayNotificationEvent({ id, dismiss: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openAnkiCardFromNotification(noteId: number): Promise<void> {
|
|
||||||
const activeIntegrationOpen = appState.ankiIntegration?.openNoteInAnki(noteId);
|
|
||||||
if (activeIntegrationOpen) {
|
|
||||||
await activeIntegrationOpen;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedConfig = getResolvedConfig();
|
|
||||||
const effectiveAnkiConfig =
|
|
||||||
appState.runtimeOptionsManager?.getEffectiveAnkiConnectConfig(resolvedConfig.ankiConnect) ??
|
|
||||||
resolvedConfig.ankiConnect;
|
|
||||||
const fallbackClient = new AnkiConnectClient(
|
|
||||||
effectiveAnkiConfig.url || DEFAULT_CONFIG.ankiConnect.url,
|
|
||||||
);
|
|
||||||
await fallbackClient.openNoteInBrowser(noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleNotificationHistoryPanel(): void {
|
|
||||||
broadcastToOverlayWindows(IPC_CHANNELS.event.notificationHistoryToggle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfiguredStatusNotification(
|
function showConfiguredStatusNotification(
|
||||||
message: string,
|
message: string,
|
||||||
options: ConfiguredStatusNotificationOptions = {},
|
options: ConfiguredStatusNotificationOptions = {},
|
||||||
): void {
|
): void {
|
||||||
notifyConfiguredStatus(
|
overlayNotificationsRuntime.showConfiguredStatusNotification(message, options);
|
||||||
message,
|
|
||||||
{
|
|
||||||
getNotificationType: () => getResolvedConfig().ankiConnect.behavior.notificationType,
|
|
||||||
isOverlayReady: () => isVisibleOverlayContentReady(),
|
|
||||||
showOsd: (text) => showMpvOsd(text),
|
|
||||||
showOverlayNotification,
|
|
||||||
showDesktopNotification: (title, notificationOptions) =>
|
|
||||||
showDesktopNotification(title, notificationOptions),
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showConfiguredPlaybackFeedback(
|
|
||||||
message: string,
|
|
||||||
options: ConfiguredStatusNotificationOptions = {},
|
|
||||||
): void {
|
|
||||||
showConfiguredStatusNotification(message, {
|
|
||||||
...getPlaybackFeedbackNotificationOptions(message),
|
|
||||||
...options,
|
|
||||||
delivery: 'feedback',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSubsyncStatusNotification(message: string): void {
|
function showSubsyncStatusNotification(message: string): void {
|
||||||
const syncing = message.startsWith('Subsync: syncing');
|
overlayNotificationsRuntime.showSubsyncStatusNotification(message);
|
||||||
const failed = message.toLowerCase().includes('failed');
|
|
||||||
showConfiguredStatusNotification(message, {
|
|
||||||
id: 'subsync-status',
|
|
||||||
title: 'Subsync',
|
|
||||||
variant: failed ? 'error' : syncing ? 'progress' : 'info',
|
|
||||||
persistent: syncing,
|
|
||||||
desktop: !syncing,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showYoutubeFlowStatusNotification(message: string): void {
|
function showYoutubeFlowStatusNotification(message: string): void {
|
||||||
const progress =
|
overlayNotificationsRuntime.showYoutubeFlowStatusNotification(message);
|
||||||
message.startsWith('Downloading subtitles') ||
|
|
||||||
message.startsWith('Loading subtitles') ||
|
|
||||||
message.startsWith('Getting subtitles') ||
|
|
||||||
message === 'Opening YouTube video';
|
|
||||||
showConfiguredStatusNotification(message, {
|
|
||||||
id: 'youtube-subtitles-status',
|
|
||||||
title: 'YouTube subtitles',
|
|
||||||
variant: progress ? 'progress' : 'info',
|
|
||||||
persistent: progress,
|
|
||||||
desktop: !progress,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOverlayLoadingOsdController(): ReturnType<typeof createOverlayLoadingOsdController> {
|
|
||||||
if (!overlayLoadingOsdController) {
|
|
||||||
overlayLoadingOsdController = createOverlayLoadingOsdController({
|
|
||||||
showOsd: (message) => {
|
|
||||||
showMpvOsd(message);
|
|
||||||
},
|
|
||||||
clearOsd: () => {
|
|
||||||
sendMpvCommandRuntime(appState.mpvClient, ['show-text', '', '1']);
|
|
||||||
},
|
|
||||||
setInterval: (callback, delayMs) => {
|
|
||||||
const timer = setInterval(callback, delayMs);
|
|
||||||
timer.unref?.();
|
|
||||||
return timer;
|
|
||||||
},
|
|
||||||
clearInterval: (timer) => {
|
|
||||||
clearInterval(timer as ReturnType<typeof setInterval>);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return overlayLoadingOsdController;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlayLoadingStatusNotification(message: string): void {
|
function showOverlayLoadingStatusNotification(message: string): void {
|
||||||
void message;
|
overlayNotificationsRuntime.showOverlayLoadingStatusNotification(message);
|
||||||
getOverlayLoadingOsdController().start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissOverlayLoadingStatusNotification(): void {
|
function dismissOverlayLoadingStatusNotification(): void {
|
||||||
getOverlayLoadingOsdController().stop();
|
overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
|
||||||
sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-overlay-loading-ready']);
|
|
||||||
dismissOverlayNotification('overlay-loading-status');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeStartOverlayLoadingOsd = createMaybeStartOverlayLoadingOsdHandler({
|
|
||||||
getVisibleOverlayRequested: () => overlayManager.getVisibleOverlayVisible(),
|
|
||||||
isOverlayContentReady: () => isVisibleOverlayContentReady(),
|
|
||||||
startOverlayLoadingOsd: () => {
|
|
||||||
showOverlayLoadingStatusNotification('Overlay loading...');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const buildBroadcastRuntimeOptionsChangedMainDepsHandler =
|
const buildBroadcastRuntimeOptionsChangedMainDepsHandler =
|
||||||
createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({
|
createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({
|
||||||
broadcastRuntimeOptionsChangedRuntime,
|
broadcastRuntimeOptionsChangedRuntime,
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ test('mpv startup signals start overlay loading OSD before readiness work', () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('overlay loading dismiss notifies mpv plugin to stop early loading OSD', () => {
|
test('overlay loading dismiss notifies mpv plugin to stop early loading OSD', () => {
|
||||||
const source = readMainSource();
|
const source = readSource('src/main/runtime/overlay-notifications-runtime.ts');
|
||||||
const dismissBlock = source.match(
|
const dismissBlock = source.match(
|
||||||
/function dismissOverlayLoadingStatusNotification\(\): void \{(?<body>[\s\S]*?)\n \}/,
|
/function dismissOverlayLoadingStatusNotification\(\): void \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
@@ -103,7 +103,7 @@ test('overlay loading dismiss notifies mpv plugin to stop early loading OSD', ()
|
|||||||
assert.ok(dismissBlock);
|
assert.ok(dismissBlock);
|
||||||
assert.match(
|
assert.match(
|
||||||
dismissBlock,
|
dismissBlock,
|
||||||
/sendMpvCommandRuntime\(appState\.mpvClient, \['script-message', 'subminer-overlay-loading-ready'\]\);/,
|
/sendMpvCommandRuntime\(deps\.getMpvClient\(\), \[\s*'script-message',\s*'subminer-overlay-loading-ready',\s*\]\);/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -168,6 +168,7 @@ test('subtitle sidebar media path tag is assigned after prefetch succeeds', () =
|
|||||||
|
|
||||||
test('update overlay notification action triggers install flow', () => {
|
test('update overlay notification action triggers install flow', () => {
|
||||||
const source = readMainSource();
|
const source = readMainSource();
|
||||||
|
const runtimeSource = readSource('src/main/runtime/overlay-notifications-runtime.ts');
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
source,
|
||||||
@@ -177,13 +178,16 @@ test('update overlay notification action triggers install flow', () => {
|
|||||||
assert.match(source, /actionId === INSTALL_UPDATE_ACTION_ID/);
|
assert.match(source, /actionId === INSTALL_UPDATE_ACTION_ID/);
|
||||||
assert.match(source, /installWhenAvailable:\s*true/);
|
assert.match(source, /installWhenAvailable:\s*true/);
|
||||||
assert.match(source, /actionId === OPEN_ANKI_CARD_ACTION_ID && noteId !== undefined/);
|
assert.match(source, /actionId === OPEN_ANKI_CARD_ACTION_ID && noteId !== undefined/);
|
||||||
assert.match(source, /appState\.ankiIntegration\?\.openNoteInAnki\(noteId\)/);
|
assert.match(runtimeSource, /deps\.getAnkiIntegration\(\)\?\.openNoteInAnki\(noteId\)/);
|
||||||
assert.match(source, /appState\.runtimeOptionsManager\?\.getEffectiveAnkiConnectConfig/);
|
|
||||||
assert.match(
|
assert.match(
|
||||||
source,
|
runtimeSource,
|
||||||
|
/deps\.getRuntimeOptionsManager\(\)\?\.getEffectiveAnkiConnectConfig/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
runtimeSource,
|
||||||
/new AnkiConnectClient\(\s*effectiveAnkiConfig\.url \|\| DEFAULT_CONFIG\.ankiConnect\.url/,
|
/new AnkiConnectClient\(\s*effectiveAnkiConfig\.url \|\| DEFAULT_CONFIG\.ankiConnect\.url/,
|
||||||
);
|
);
|
||||||
assert.match(source, /fallbackClient\.openNoteInBrowser\(noteId\)/);
|
assert.match(runtimeSource, /fallbackClient\.openNoteInBrowser\(noteId\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('subtitle change re-prioritizes prefetch around live playback before tokenizing current line', () => {
|
test('subtitle change re-prioritizes prefetch around live playback before tokenizing current line', () => {
|
||||||
@@ -463,7 +467,7 @@ test('manual visible overlay hide dismisses loading OSD', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('configured overlay notifications require visible ready overlay window', () => {
|
test('configured overlay notifications require visible ready overlay window', () => {
|
||||||
const source = readMainSource();
|
const source = readSource('src/main/runtime/overlay-notifications-runtime.ts');
|
||||||
const readinessBlock = source.match(
|
const readinessBlock = source.match(
|
||||||
/function isVisibleOverlayContentReady\(\): boolean \{(?<body>[\s\S]*?)\n \}/,
|
/function isVisibleOverlayContentReady\(\): boolean \{(?<body>[\s\S]*?)\n \}/,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
@@ -473,7 +477,7 @@ test('configured overlay notifications require visible ready overlay window', ()
|
|||||||
|
|
||||||
assert.ok(readinessBlock);
|
assert.ok(readinessBlock);
|
||||||
assert.ok(statusBlock);
|
assert.ok(statusBlock);
|
||||||
assert.match(readinessBlock, /overlayManager\.getVisibleOverlayVisible\(\)/);
|
assert.match(readinessBlock, /deps\.getVisibleOverlayVisible\(\)/);
|
||||||
assert.match(readinessBlock, /isOverlayWindowReadyForNotification\(overlayWindow\)/);
|
assert.match(readinessBlock, /isOverlayWindowReadyForNotification\(overlayWindow\)/);
|
||||||
assert.doesNotMatch(readinessBlock, /isOverlayWindowContentReady\(overlayWindow\)/);
|
assert.doesNotMatch(readinessBlock, /isOverlayWindowContentReady\(overlayWindow\)/);
|
||||||
assert.match(statusBlock, /isOverlayReady: \(\) => isVisibleOverlayContentReady\(\)/);
|
assert.match(statusBlock, /isOverlayReady: \(\) => isVisibleOverlayContentReady\(\)/);
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import type { BrowserWindow } from 'electron';
|
||||||
|
import type {
|
||||||
|
NotificationType,
|
||||||
|
OverlayNotificationEventPayload,
|
||||||
|
OverlayNotificationPayload,
|
||||||
|
ResolvedConfig,
|
||||||
|
} from '../../types';
|
||||||
|
import type { AnkiIntegration } from '../../anki-integration';
|
||||||
|
import type { RuntimeOptionsManager } from '../../runtime-options';
|
||||||
|
import { AnkiConnectClient } from '../../anki-connect';
|
||||||
|
import { DEFAULT_CONFIG } from '../../config';
|
||||||
|
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
|
||||||
|
import { showDesktopNotification } from '../../core/utils';
|
||||||
|
import {
|
||||||
|
isOverlayWindowContentReady,
|
||||||
|
sendMpvCommandRuntime,
|
||||||
|
type MpvIpcClient,
|
||||||
|
} from '../../core/services';
|
||||||
|
import { createOverlayLoadingOsdController } from './overlay-loading-osd';
|
||||||
|
import { createMaybeStartOverlayLoadingOsdHandler } from './overlay-loading-osd-start';
|
||||||
|
import { withConfiguredOverlayNotificationPosition } from './overlay-notification-position';
|
||||||
|
import { createOverlayNotificationDelivery } from './overlay-notification-delivery';
|
||||||
|
import {
|
||||||
|
getPlaybackFeedbackNotificationOptions,
|
||||||
|
notifyConfiguredStatus,
|
||||||
|
type ConfiguredStatusNotificationOptions,
|
||||||
|
} from './configured-status-notification';
|
||||||
|
import { resolveOverlayReadinessNotificationType } from './notification-routing';
|
||||||
|
|
||||||
|
export interface OverlayNotificationsRuntimeDeps {
|
||||||
|
getResolvedConfig: () => ResolvedConfig;
|
||||||
|
getMainOverlayWindow: () => BrowserWindow | null;
|
||||||
|
getVisibleOverlayVisible: () => boolean;
|
||||||
|
broadcastToOverlayWindows: (channel: string, ...args: unknown[]) => void;
|
||||||
|
showMpvOsd: (message: string) => void;
|
||||||
|
getMpvClient: () => MpvIpcClient | null;
|
||||||
|
getAnkiIntegration: () => AnkiIntegration | null;
|
||||||
|
getRuntimeOptionsManager: () => RuntimeOptionsManager | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOverlayNotificationsRuntime(deps: OverlayNotificationsRuntimeDeps): {
|
||||||
|
isVisibleOverlayContentReady: () => boolean;
|
||||||
|
getConfiguredStatusNotificationType: () => NotificationType;
|
||||||
|
flushQueuedOverlayNotifications: () => void;
|
||||||
|
showOverlayNotification: (payload: OverlayNotificationPayload) => void;
|
||||||
|
dismissOverlayNotification: (id: string) => void;
|
||||||
|
openAnkiCardFromNotification: (noteId: number) => Promise<void>;
|
||||||
|
toggleNotificationHistoryPanel: () => void;
|
||||||
|
showConfiguredStatusNotification: (
|
||||||
|
message: string,
|
||||||
|
options?: ConfiguredStatusNotificationOptions,
|
||||||
|
) => void;
|
||||||
|
showConfiguredPlaybackFeedback: (
|
||||||
|
message: string,
|
||||||
|
options?: ConfiguredStatusNotificationOptions,
|
||||||
|
) => void;
|
||||||
|
showSubsyncStatusNotification: (message: string) => void;
|
||||||
|
showYoutubeFlowStatusNotification: (message: string) => void;
|
||||||
|
showOverlayLoadingStatusNotification: (message: string) => void;
|
||||||
|
dismissOverlayLoadingStatusNotification: () => void;
|
||||||
|
maybeStartOverlayLoadingOsd: (mediaPath?: string | null) => void;
|
||||||
|
} {
|
||||||
|
function isVisibleOverlayContentReady(): boolean {
|
||||||
|
const overlayWindow = deps.getMainOverlayWindow();
|
||||||
|
return Boolean(
|
||||||
|
deps.getVisibleOverlayVisible() &&
|
||||||
|
overlayWindow &&
|
||||||
|
isOverlayWindowReadyForNotification(overlayWindow),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredStatusNotificationType(): NotificationType {
|
||||||
|
const configuredType = deps.getResolvedConfig().ankiConnect.behavior.notificationType;
|
||||||
|
return resolveOverlayReadinessNotificationType(configuredType, isVisibleOverlayContentReady());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOverlayWindowReadyForNotification(window: BrowserWindow): boolean {
|
||||||
|
if (window.isDestroyed() || !isOverlayWindowContentReady(window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (window.webContents.isLoading()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const currentURL = window.webContents.getURL();
|
||||||
|
return currentURL !== '' && currentURL !== 'about:blank';
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlayNotificationDelivery = createOverlayNotificationDelivery({
|
||||||
|
hasReadyOverlayWindow: () => isVisibleOverlayContentReady(),
|
||||||
|
send: (payload) => {
|
||||||
|
deps.broadcastToOverlayWindows(IPC_CHANNELS.event.overlayNotification, payload);
|
||||||
|
},
|
||||||
|
scheduleFlushRetry: (callback, delayMs) => setTimeout(callback, delayMs),
|
||||||
|
clearFlushRetry: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
||||||
|
});
|
||||||
|
let overlayLoadingOsdController: ReturnType<typeof createOverlayLoadingOsdController> | null =
|
||||||
|
null;
|
||||||
|
|
||||||
|
function flushQueuedOverlayNotifications(): void {
|
||||||
|
overlayNotificationDelivery.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void {
|
||||||
|
overlayNotificationDelivery.send(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlayNotification(payload: OverlayNotificationPayload): void {
|
||||||
|
sendOverlayNotificationEvent(
|
||||||
|
withConfiguredOverlayNotificationPosition(payload, deps.getResolvedConfig()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissOverlayNotification(id: string): void {
|
||||||
|
sendOverlayNotificationEvent({ id, dismiss: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openAnkiCardFromNotification(noteId: number): Promise<void> {
|
||||||
|
const activeIntegrationOpen = deps.getAnkiIntegration()?.openNoteInAnki(noteId);
|
||||||
|
if (activeIntegrationOpen) {
|
||||||
|
await activeIntegrationOpen;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedConfig = deps.getResolvedConfig();
|
||||||
|
const effectiveAnkiConfig =
|
||||||
|
deps.getRuntimeOptionsManager()?.getEffectiveAnkiConnectConfig(resolvedConfig.ankiConnect) ??
|
||||||
|
resolvedConfig.ankiConnect;
|
||||||
|
const fallbackClient = new AnkiConnectClient(
|
||||||
|
effectiveAnkiConfig.url || DEFAULT_CONFIG.ankiConnect.url,
|
||||||
|
);
|
||||||
|
await fallbackClient.openNoteInBrowser(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleNotificationHistoryPanel(): void {
|
||||||
|
deps.broadcastToOverlayWindows(IPC_CHANNELS.event.notificationHistoryToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showConfiguredStatusNotification(
|
||||||
|
message: string,
|
||||||
|
options: ConfiguredStatusNotificationOptions = {},
|
||||||
|
): void {
|
||||||
|
notifyConfiguredStatus(
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
getNotificationType: () => deps.getResolvedConfig().ankiConnect.behavior.notificationType,
|
||||||
|
isOverlayReady: () => isVisibleOverlayContentReady(),
|
||||||
|
showOsd: (text) => deps.showMpvOsd(text),
|
||||||
|
showOverlayNotification,
|
||||||
|
showDesktopNotification: (title, notificationOptions) =>
|
||||||
|
showDesktopNotification(title, notificationOptions),
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showConfiguredPlaybackFeedback(
|
||||||
|
message: string,
|
||||||
|
options: ConfiguredStatusNotificationOptions = {},
|
||||||
|
): void {
|
||||||
|
showConfiguredStatusNotification(message, {
|
||||||
|
...getPlaybackFeedbackNotificationOptions(message),
|
||||||
|
...options,
|
||||||
|
delivery: 'feedback',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSubsyncStatusNotification(message: string): void {
|
||||||
|
const syncing = message.startsWith('Subsync: syncing');
|
||||||
|
const failed = message.toLowerCase().includes('failed');
|
||||||
|
showConfiguredStatusNotification(message, {
|
||||||
|
id: 'subsync-status',
|
||||||
|
title: 'Subsync',
|
||||||
|
variant: failed ? 'error' : syncing ? 'progress' : 'info',
|
||||||
|
persistent: syncing,
|
||||||
|
desktop: !syncing,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showYoutubeFlowStatusNotification(message: string): void {
|
||||||
|
const progress =
|
||||||
|
message.startsWith('Downloading subtitles') ||
|
||||||
|
message.startsWith('Loading subtitles') ||
|
||||||
|
message.startsWith('Getting subtitles') ||
|
||||||
|
message === 'Opening YouTube video';
|
||||||
|
showConfiguredStatusNotification(message, {
|
||||||
|
id: 'youtube-subtitles-status',
|
||||||
|
title: 'YouTube subtitles',
|
||||||
|
variant: progress ? 'progress' : 'info',
|
||||||
|
persistent: progress,
|
||||||
|
desktop: !progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOverlayLoadingOsdController(): ReturnType<typeof createOverlayLoadingOsdController> {
|
||||||
|
if (!overlayLoadingOsdController) {
|
||||||
|
overlayLoadingOsdController = createOverlayLoadingOsdController({
|
||||||
|
showOsd: (message) => {
|
||||||
|
deps.showMpvOsd(message);
|
||||||
|
},
|
||||||
|
clearOsd: () => {
|
||||||
|
sendMpvCommandRuntime(deps.getMpvClient(), ['show-text', '', '1']);
|
||||||
|
},
|
||||||
|
setInterval: (callback, delayMs) => {
|
||||||
|
const timer = setInterval(callback, delayMs);
|
||||||
|
timer.unref?.();
|
||||||
|
return timer;
|
||||||
|
},
|
||||||
|
clearInterval: (timer) => {
|
||||||
|
clearInterval(timer as ReturnType<typeof setInterval>);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return overlayLoadingOsdController;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlayLoadingStatusNotification(message: string): void {
|
||||||
|
void message;
|
||||||
|
getOverlayLoadingOsdController().start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissOverlayLoadingStatusNotification(): void {
|
||||||
|
getOverlayLoadingOsdController().stop();
|
||||||
|
sendMpvCommandRuntime(deps.getMpvClient(), [
|
||||||
|
'script-message',
|
||||||
|
'subminer-overlay-loading-ready',
|
||||||
|
]);
|
||||||
|
dismissOverlayNotification('overlay-loading-status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeStartOverlayLoadingOsd = createMaybeStartOverlayLoadingOsdHandler({
|
||||||
|
getVisibleOverlayRequested: () => deps.getVisibleOverlayVisible(),
|
||||||
|
isOverlayContentReady: () => isVisibleOverlayContentReady(),
|
||||||
|
startOverlayLoadingOsd: () => {
|
||||||
|
showOverlayLoadingStatusNotification('Overlay loading...');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisibleOverlayContentReady,
|
||||||
|
getConfiguredStatusNotificationType,
|
||||||
|
flushQueuedOverlayNotifications,
|
||||||
|
showOverlayNotification,
|
||||||
|
dismissOverlayNotification,
|
||||||
|
openAnkiCardFromNotification,
|
||||||
|
toggleNotificationHistoryPanel,
|
||||||
|
showConfiguredStatusNotification,
|
||||||
|
showConfiguredPlaybackFeedback,
|
||||||
|
showSubsyncStatusNotification,
|
||||||
|
showYoutubeFlowStatusNotification,
|
||||||
|
showOverlayLoadingStatusNotification,
|
||||||
|
dismissOverlayLoadingStatusNotification,
|
||||||
|
maybeStartOverlayLoadingOsd,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user