mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 15:13:32 -07:00
feat(overlay): add loading OSD spinner and queue notifications until ren
- Show mpv OSD spinner from start-file until subminer-overlay-loading-ready; force-shown for visible-overlay startup regardless of osd_messages setting - Gate non-macOS overlay visibility on content-ready so first subtitle line is immediately hoverable and clickable - Queue startup notifications in main process until overlay window finishes loading; upsert progress cards by id to avoid cold-start floods - Defer background warmups until after overlay runtime init so queued notifications can deliver promptly - Preserve character dictionary checking/building/importing/ready phases as distinct history entries; route building and importing to system notifications when notificationType is both
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import type { OverlayNotificationEventPayload } from '../../types/notification';
|
||||
|
||||
export interface OverlayNotificationDeliveryDeps {
|
||||
hasReadyOverlayWindow: () => boolean;
|
||||
send: (payload: OverlayNotificationEventPayload) => void;
|
||||
maxQueuedEvents?: number;
|
||||
flushRetryDelayMs?: number;
|
||||
terminalUpdateDelayMs?: number;
|
||||
scheduleFlushRetry?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearFlushRetry?: (handle: unknown) => void;
|
||||
}
|
||||
|
||||
function getPayloadId(payload: OverlayNotificationEventPayload): string | null {
|
||||
return typeof payload.id === 'string' && payload.id.trim().length > 0 ? payload.id : null;
|
||||
}
|
||||
|
||||
function getPayloadHistoryId(payload: OverlayNotificationEventPayload): string | null {
|
||||
if ('dismiss' in payload) {
|
||||
return null;
|
||||
}
|
||||
return typeof payload.historyId === 'string' && payload.historyId.trim().length > 0
|
||||
? payload.historyId
|
||||
: null;
|
||||
}
|
||||
|
||||
function isDismissPayload(
|
||||
payload: OverlayNotificationEventPayload,
|
||||
): payload is Extract<OverlayNotificationEventPayload, { dismiss: true }> {
|
||||
return 'dismiss' in payload && payload.dismiss === true;
|
||||
}
|
||||
|
||||
export function createOverlayNotificationDelivery(deps: OverlayNotificationDeliveryDeps): {
|
||||
send: (payload: OverlayNotificationEventPayload) => void;
|
||||
flush: () => void;
|
||||
getQueuedCount: () => number;
|
||||
} {
|
||||
const maxQueuedEvents = Math.max(1, deps.maxQueuedEvents ?? 32);
|
||||
const flushRetryDelayMs = Math.max(1, deps.flushRetryDelayMs ?? 50);
|
||||
const terminalUpdateDelayMs = Math.max(1, deps.terminalUpdateDelayMs ?? 750);
|
||||
const queuedEvents: OverlayNotificationEventPayload[] = [];
|
||||
let flushRetryHandle: unknown = null;
|
||||
|
||||
const removeQueuedPayloadsById = (id: string): void => {
|
||||
const nextEvents = queuedEvents.filter((queued) => getPayloadId(queued) !== id);
|
||||
queuedEvents.splice(0, queuedEvents.length, ...nextEvents);
|
||||
};
|
||||
|
||||
const clearFlushRetry = (): void => {
|
||||
if (flushRetryHandle === null) {
|
||||
return;
|
||||
}
|
||||
deps.clearFlushRetry?.(flushRetryHandle);
|
||||
flushRetryHandle = null;
|
||||
};
|
||||
|
||||
const scheduleFlushRetry = (delayMs = flushRetryDelayMs): void => {
|
||||
if (!deps.scheduleFlushRetry || flushRetryHandle !== null || queuedEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
flushRetryHandle = deps.scheduleFlushRetry(() => {
|
||||
flushRetryHandle = null;
|
||||
flush();
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const queuePayload = (payload: OverlayNotificationEventPayload): void => {
|
||||
const id = getPayloadId(payload);
|
||||
if (isDismissPayload(payload)) {
|
||||
if (id) {
|
||||
removeQueuedPayloadsById(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
const payloadPersistent = payload.persistent === true;
|
||||
const payloadHistoryId = getPayloadHistoryId(payload);
|
||||
const existingIndex = queuedEvents.findIndex(
|
||||
(queued) =>
|
||||
getPayloadId(queued) === id &&
|
||||
!isDismissPayload(queued) &&
|
||||
getPayloadHistoryId(queued) === payloadHistoryId &&
|
||||
(queued.persistent === true) === payloadPersistent,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
queuedEvents[existingIndex] = payload;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
queuedEvents.push(payload);
|
||||
while (queuedEvents.length > maxQueuedEvents) {
|
||||
queuedEvents.shift();
|
||||
}
|
||||
};
|
||||
|
||||
const flush = (): void => {
|
||||
if (!deps.hasReadyOverlayWindow()) {
|
||||
scheduleFlushRetry();
|
||||
return;
|
||||
}
|
||||
clearFlushRetry();
|
||||
const readyEvents = queuedEvents.splice(0, queuedEvents.length);
|
||||
const sentPersistentIds = new Set<string>();
|
||||
const deferredTerminalEvents: OverlayNotificationEventPayload[] = [];
|
||||
for (const payload of readyEvents) {
|
||||
const id = getPayloadId(payload);
|
||||
if (
|
||||
id &&
|
||||
!isDismissPayload(payload) &&
|
||||
payload.persistent !== true &&
|
||||
sentPersistentIds.has(id)
|
||||
) {
|
||||
deferredTerminalEvents.push(payload);
|
||||
continue;
|
||||
}
|
||||
deps.send(payload);
|
||||
if (id && !isDismissPayload(payload) && payload.persistent === true) {
|
||||
sentPersistentIds.add(id);
|
||||
}
|
||||
}
|
||||
if (deferredTerminalEvents.length > 0) {
|
||||
if (!deps.scheduleFlushRetry) {
|
||||
for (const payload of deferredTerminalEvents) {
|
||||
deps.send(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
queuedEvents.unshift(...deferredTerminalEvents);
|
||||
scheduleFlushRetry(terminalUpdateDelayMs);
|
||||
}
|
||||
};
|
||||
|
||||
const send = (payload: OverlayNotificationEventPayload): void => {
|
||||
if (isDismissPayload(payload)) {
|
||||
const id = getPayloadId(payload);
|
||||
if (id) {
|
||||
removeQueuedPayloadsById(id);
|
||||
}
|
||||
if (deps.hasReadyOverlayWindow()) {
|
||||
deps.send(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deps.hasReadyOverlayWindow()) {
|
||||
queuePayload(payload);
|
||||
return;
|
||||
}
|
||||
deps.send(payload);
|
||||
};
|
||||
|
||||
return {
|
||||
send,
|
||||
flush,
|
||||
getQueuedCount: () => queuedEvents.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user