mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-06-09 15:13:32 -07:00
feat(notifications): add overlay notifications with position config
- Add Catppuccin Macchiato overlay notification stack with 3s transient timeout - Add `notifications.overlayPosition` config (top-left | top | top-right) - Route startup tokenization and subtitle annotation status through configured surfaces - Deduplicate rapid subtitle mode toggle notifications - Change `both` to mean overlay + system; add `osd-system` as legacy alias for old behavior - Keep `osd`/`osd-system` as config-file-only legacy values; Settings UI offers overlay/system/both/none
This commit is contained in:
@@ -1,10 +1,41 @@
|
||||
import type { NotificationType, OverlayNotificationPayload } from '../../types/notification';
|
||||
|
||||
export interface StartupOsdSequencerCharacterDictionaryEvent {
|
||||
phase: 'checking' | 'generating' | 'syncing' | 'building' | 'importing' | 'ready' | 'failed';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function createStartupOsdSequencer(deps: { showOsd: (message: string) => boolean | void }): {
|
||||
export interface StartupOsdSequencerDeps {
|
||||
getNotificationType?: () => NotificationType | undefined;
|
||||
showOsd: (message: string) => boolean | void;
|
||||
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
|
||||
showDesktopNotification?: (title: string, options: { body?: string }) => void;
|
||||
}
|
||||
|
||||
interface StartupStatusNotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
variant: OverlayNotificationPayload['variant'];
|
||||
persistent: boolean;
|
||||
desktop?: boolean;
|
||||
}
|
||||
|
||||
function shouldShowOsd(type: NotificationType): boolean {
|
||||
return type === 'osd' || type === 'osd-system';
|
||||
}
|
||||
|
||||
function shouldShowOverlay(type: NotificationType): boolean {
|
||||
return type === 'overlay' || type === 'both';
|
||||
}
|
||||
|
||||
function shouldShowDesktop(type: NotificationType): boolean {
|
||||
return type === 'system' || type === 'both' || type === 'osd-system';
|
||||
}
|
||||
|
||||
export function createStartupOsdSequencer(deps: StartupOsdSequencerDeps): {
|
||||
reset: () => void;
|
||||
showTokenizationLoading: (message: string) => void;
|
||||
markTokenizationReady: () => void;
|
||||
showAnnotationLoading: (message: string) => void;
|
||||
markAnnotationLoadingComplete: (message: string) => void;
|
||||
@@ -12,6 +43,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
} {
|
||||
let tokenizationReady = false;
|
||||
let tokenizationWarmupCompleted = false;
|
||||
let tokenizationLoadingShown = false;
|
||||
let annotationLoadingMessage: string | null = null;
|
||||
let pendingDictionaryProgress: StartupOsdSequencerCharacterDictionaryEvent | null = null;
|
||||
let pendingDictionaryFailure: StartupOsdSequencerCharacterDictionaryEvent | null = null;
|
||||
@@ -20,7 +52,66 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
|
||||
const canShowDictionaryStatus = (): boolean =>
|
||||
tokenizationReady && annotationLoadingMessage === null;
|
||||
const showOsd = (message: string): boolean => deps.showOsd(message) !== false;
|
||||
const getNotificationType = (): NotificationType => deps.getNotificationType?.() ?? 'osd';
|
||||
const notifyStartupStatus = (options: StartupStatusNotificationOptions): boolean => {
|
||||
const type = getNotificationType();
|
||||
if (type === 'none') {
|
||||
return false;
|
||||
}
|
||||
let shown = false;
|
||||
if (shouldShowOverlay(type)) {
|
||||
deps.showOverlayNotification?.({
|
||||
id: options.id,
|
||||
title: options.title,
|
||||
body: options.message,
|
||||
variant: options.variant,
|
||||
persistent: options.persistent,
|
||||
});
|
||||
shown = true;
|
||||
}
|
||||
if (shouldShowOsd(type)) {
|
||||
shown = deps.showOsd(options.message) !== false || shown;
|
||||
}
|
||||
if (options.desktop !== false && shouldShowDesktop(type)) {
|
||||
deps.showDesktopNotification?.('SubMiner', { body: options.message });
|
||||
shown = true;
|
||||
}
|
||||
return shown;
|
||||
};
|
||||
const showOsd = (message: string): boolean =>
|
||||
notifyStartupStatus({
|
||||
id: 'startup-status',
|
||||
title: 'SubMiner',
|
||||
message,
|
||||
variant: 'info',
|
||||
persistent: false,
|
||||
});
|
||||
const notifyTokenization = (
|
||||
message: string,
|
||||
variant: OverlayNotificationPayload['variant'],
|
||||
persistent: boolean,
|
||||
): boolean =>
|
||||
notifyStartupStatus({
|
||||
id: 'startup-tokenization',
|
||||
title: 'Subtitle tokenization',
|
||||
message,
|
||||
variant,
|
||||
persistent,
|
||||
desktop: !persistent,
|
||||
});
|
||||
const notifyAnnotation = (
|
||||
message: string,
|
||||
variant: OverlayNotificationPayload['variant'],
|
||||
persistent: boolean,
|
||||
): boolean =>
|
||||
notifyStartupStatus({
|
||||
id: 'startup-subtitle-annotations',
|
||||
title: 'Subtitle annotations',
|
||||
message,
|
||||
variant,
|
||||
persistent,
|
||||
desktop: !persistent,
|
||||
});
|
||||
|
||||
const flushBufferedDictionaryStatus = (): boolean => {
|
||||
if (!canShowDictionaryStatus()) {
|
||||
@@ -55,17 +146,29 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
return {
|
||||
reset: () => {
|
||||
tokenizationReady = tokenizationWarmupCompleted;
|
||||
tokenizationLoadingShown = false;
|
||||
annotationLoadingMessage = null;
|
||||
pendingDictionaryProgress = null;
|
||||
pendingDictionaryFailure = null;
|
||||
pendingDictionaryReady = null;
|
||||
dictionaryProgressShown = false;
|
||||
},
|
||||
showTokenizationLoading: (message) => {
|
||||
if (tokenizationReady) {
|
||||
return;
|
||||
}
|
||||
tokenizationLoadingShown = true;
|
||||
notifyTokenization(message, 'progress', true);
|
||||
},
|
||||
markTokenizationReady: () => {
|
||||
tokenizationWarmupCompleted = true;
|
||||
tokenizationReady = true;
|
||||
if (tokenizationLoadingShown) {
|
||||
notifyTokenization('Subtitle tokenization ready', 'success', false);
|
||||
tokenizationLoadingShown = false;
|
||||
}
|
||||
if (annotationLoadingMessage !== null) {
|
||||
showOsd(annotationLoadingMessage);
|
||||
notifyAnnotation(annotationLoadingMessage, 'progress', true);
|
||||
return;
|
||||
}
|
||||
flushBufferedDictionaryStatus();
|
||||
@@ -73,7 +176,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
showAnnotationLoading: (message) => {
|
||||
annotationLoadingMessage = message;
|
||||
if (tokenizationReady) {
|
||||
showOsd(message);
|
||||
notifyAnnotation(message, 'progress', true);
|
||||
}
|
||||
},
|
||||
markAnnotationLoadingComplete: (message) => {
|
||||
@@ -84,7 +187,7 @@ export function createStartupOsdSequencer(deps: { showOsd: (message: string) =>
|
||||
if (flushBufferedDictionaryStatus()) {
|
||||
return;
|
||||
}
|
||||
showOsd(message);
|
||||
notifyAnnotation(message, 'success', false);
|
||||
},
|
||||
notifyCharacterDictionaryStatus: (event) => {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user