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,
|
||||
SubtitlePosition,
|
||||
OverlayNotificationPayload,
|
||||
OverlayNotificationEventPayload,
|
||||
NotificationType,
|
||||
WindowGeometry,
|
||||
} from './types';
|
||||
@@ -545,16 +544,8 @@ import {
|
||||
INSTALL_UPDATE_ACTION_ID,
|
||||
UPDATE_AVAILABLE_NOTIFICATION_ID,
|
||||
} from './main/runtime/update/update-notifications';
|
||||
import { createOverlayLoadingOsdController } from './main/runtime/overlay-loading-osd';
|
||||
import { createMaybeStartOverlayLoadingOsdHandler } from './main/runtime/overlay-loading-osd-start';
|
||||
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 { createOverlayNotificationsRuntime } from './main/runtime/overlay-notifications-runtime';
|
||||
import { type ConfiguredStatusNotificationOptions } from './main/runtime/configured-status-notification';
|
||||
import {
|
||||
runUpdateCliCommand,
|
||||
writeUpdateCliCommandResponse,
|
||||
@@ -3291,177 +3282,58 @@ function broadcastToOverlayWindows(channel: string, ...args: unknown[]): void {
|
||||
overlayManager.broadcastToOverlayWindows(channel, ...args);
|
||||
}
|
||||
|
||||
function isVisibleOverlayContentReady(): boolean {
|
||||
const overlayWindow = overlayManager.getMainWindow();
|
||||
return Boolean(
|
||||
overlayManager.getVisibleOverlayVisible() &&
|
||||
overlayWindow &&
|
||||
isOverlayWindowReadyForNotification(overlayWindow),
|
||||
);
|
||||
}
|
||||
|
||||
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>),
|
||||
const overlayNotificationsRuntime = createOverlayNotificationsRuntime({
|
||||
getResolvedConfig: () => getResolvedConfig(),
|
||||
getMainOverlayWindow: () => overlayManager.getMainWindow(),
|
||||
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
|
||||
broadcastToOverlayWindows: (channel, ...args) => broadcastToOverlayWindows(channel, ...args),
|
||||
showMpvOsd: (message) => showMpvOsd(message),
|
||||
getMpvClient: () => appState.mpvClient,
|
||||
getAnkiIntegration: () => appState.ankiIntegration,
|
||||
getRuntimeOptionsManager: () => appState.runtimeOptionsManager,
|
||||
});
|
||||
let overlayLoadingOsdController: ReturnType<typeof createOverlayLoadingOsdController> | null = null;
|
||||
const {
|
||||
flushQueuedOverlayNotifications,
|
||||
openAnkiCardFromNotification,
|
||||
toggleNotificationHistoryPanel,
|
||||
showConfiguredPlaybackFeedback,
|
||||
maybeStartOverlayLoadingOsd,
|
||||
} = overlayNotificationsRuntime;
|
||||
|
||||
function flushQueuedOverlayNotifications(): void {
|
||||
overlayNotificationDelivery.flush();
|
||||
}
|
||||
|
||||
function sendOverlayNotificationEvent(payload: OverlayNotificationEventPayload): void {
|
||||
overlayNotificationDelivery.send(payload);
|
||||
// Hoisted wrappers: these names are referenced (directly or via deps object
|
||||
// literals) during module initialization before this point, so they must stay
|
||||
// hoisted function declarations that delegate to the runtime lazily.
|
||||
function getConfiguredStatusNotificationType(): NotificationType {
|
||||
return overlayNotificationsRuntime.getConfiguredStatusNotificationType();
|
||||
}
|
||||
|
||||
function showOverlayNotification(payload: OverlayNotificationPayload): void {
|
||||
sendOverlayNotificationEvent(
|
||||
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);
|
||||
overlayNotificationsRuntime.showOverlayNotification(payload);
|
||||
}
|
||||
|
||||
function showConfiguredStatusNotification(
|
||||
message: string,
|
||||
options: ConfiguredStatusNotificationOptions = {},
|
||||
): void {
|
||||
notifyConfiguredStatus(
|
||||
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',
|
||||
});
|
||||
overlayNotificationsRuntime.showConfiguredStatusNotification(message, options);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
overlayNotificationsRuntime.showSubsyncStatusNotification(message);
|
||||
}
|
||||
|
||||
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) => {
|
||||
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;
|
||||
overlayNotificationsRuntime.showYoutubeFlowStatusNotification(message);
|
||||
}
|
||||
|
||||
function showOverlayLoadingStatusNotification(message: string): void {
|
||||
void message;
|
||||
getOverlayLoadingOsdController().start();
|
||||
overlayNotificationsRuntime.showOverlayLoadingStatusNotification(message);
|
||||
}
|
||||
|
||||
function dismissOverlayLoadingStatusNotification(): void {
|
||||
getOverlayLoadingOsdController().stop();
|
||||
sendMpvCommandRuntime(appState.mpvClient, ['script-message', 'subminer-overlay-loading-ready']);
|
||||
dismissOverlayNotification('overlay-loading-status');
|
||||
overlayNotificationsRuntime.dismissOverlayLoadingStatusNotification();
|
||||
}
|
||||
|
||||
const maybeStartOverlayLoadingOsd = createMaybeStartOverlayLoadingOsdHandler({
|
||||
getVisibleOverlayRequested: () => overlayManager.getVisibleOverlayVisible(),
|
||||
isOverlayContentReady: () => isVisibleOverlayContentReady(),
|
||||
startOverlayLoadingOsd: () => {
|
||||
showOverlayLoadingStatusNotification('Overlay loading...');
|
||||
},
|
||||
});
|
||||
|
||||
const buildBroadcastRuntimeOptionsChangedMainDepsHandler =
|
||||
createBuildBroadcastRuntimeOptionsChangedMainDepsHandler({
|
||||
broadcastRuntimeOptionsChangedRuntime,
|
||||
|
||||
@@ -95,15 +95,15 @@ test('mpv startup signals start overlay loading OSD before readiness work', () =
|
||||
});
|
||||
|
||||
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(
|
||||
/function dismissOverlayLoadingStatusNotification\(\): void \{(?<body>[\s\S]*?)\n\}/,
|
||||
/function dismissOverlayLoadingStatusNotification\(\): void \{(?<body>[\s\S]*?)\n \}/,
|
||||
)?.groups?.body;
|
||||
|
||||
assert.ok(dismissBlock);
|
||||
assert.match(
|
||||
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', () => {
|
||||
const source = readMainSource();
|
||||
const runtimeSource = readSource('src/main/runtime/overlay-notifications-runtime.ts');
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
@@ -177,13 +178,16 @@ test('update overlay notification action triggers install flow', () => {
|
||||
assert.match(source, /actionId === INSTALL_UPDATE_ACTION_ID/);
|
||||
assert.match(source, /installWhenAvailable:\s*true/);
|
||||
assert.match(source, /actionId === OPEN_ANKI_CARD_ACTION_ID && noteId !== undefined/);
|
||||
assert.match(source, /appState\.ankiIntegration\?\.openNoteInAnki\(noteId\)/);
|
||||
assert.match(source, /appState\.runtimeOptionsManager\?\.getEffectiveAnkiConnectConfig/);
|
||||
assert.match(runtimeSource, /deps\.getAnkiIntegration\(\)\?\.openNoteInAnki\(noteId\)/);
|
||||
assert.match(
|
||||
source,
|
||||
runtimeSource,
|
||||
/deps\.getRuntimeOptionsManager\(\)\?\.getEffectiveAnkiConnectConfig/,
|
||||
);
|
||||
assert.match(
|
||||
runtimeSource,
|
||||
/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', () => {
|
||||
@@ -463,17 +467,17 @@ test('manual visible overlay hide dismisses loading OSD', () => {
|
||||
});
|
||||
|
||||
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(
|
||||
/function isVisibleOverlayContentReady\(\): boolean \{(?<body>[\s\S]*?)\n\}/,
|
||||
/function isVisibleOverlayContentReady\(\): boolean \{(?<body>[\s\S]*?)\n \}/,
|
||||
)?.groups?.body;
|
||||
const statusBlock = source.match(
|
||||
/function showConfiguredStatusNotification\([\s\S]*?\): void \{(?<body>[\s\S]*?)\n\}/,
|
||||
/function showConfiguredStatusNotification\([\s\S]*?\): void \{(?<body>[\s\S]*?)\n \}/,
|
||||
)?.groups?.body;
|
||||
|
||||
assert.ok(readinessBlock);
|
||||
assert.ok(statusBlock);
|
||||
assert.match(readinessBlock, /overlayManager\.getVisibleOverlayVisible\(\)/);
|
||||
assert.match(readinessBlock, /deps\.getVisibleOverlayVisible\(\)/);
|
||||
assert.match(readinessBlock, /isOverlayWindowReadyForNotification\(overlayWindow\)/);
|
||||
assert.doesNotMatch(readinessBlock, /isOverlayWindowContentReady\(overlayWindow\)/);
|
||||
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