Files
homarr/packages/widgets/src/common/browser-alert.ts
T
2026-08-20 15:16:26 +02:00

88 lines
3.1 KiB
TypeScript

export type BrowserNotificationPermission = NotificationPermission | "unsupported";
export interface BrowserNotificationOptions {
title: string;
body?: string;
icon?: string;
tag?: string;
}
const maxRememberedOccurrences = 100;
export const getBrowserNotificationPermission = (): BrowserNotificationPermission => {
if (typeof window === "undefined" || !("Notification" in window)) return "unsupported";
return window.Notification.permission;
};
export const requestBrowserNotificationPermission = async (): Promise<BrowserNotificationPermission> => {
if (typeof window === "undefined" || !("Notification" in window)) return "unsupported";
try {
return await window.Notification.requestPermission();
} catch {
return getBrowserNotificationPermission();
}
};
export const showBrowserNotification = ({ title, body, icon, tag }: BrowserNotificationOptions): boolean => {
if (typeof window === "undefined" || !("Notification" in window) || window.Notification.permission !== "granted") {
return false;
}
try {
const notification = new window.Notification(title, { body, icon, tag });
void notification;
return true;
} catch {
return false;
}
};
export const playBrowserAlertSound = (): boolean => {
if (typeof window === "undefined") return false;
const AudioContextConstructor =
window.AudioContext ?? (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AudioContextConstructor) return false;
try {
const context = new AudioContextConstructor();
const oscillator = context.createOscillator();
const gain = context.createGain();
const start = context.currentTime;
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(880, start);
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(0.18, start + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, start + 0.3);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start(start);
oscillator.stop(start + 0.32);
oscillator.addEventListener("ended", () => void context.close(), { once: true });
return true;
} catch {
return false;
}
};
export const addRememberedBrowserAlert = (occurrences: string[], occurrence: string): string[] => {
const withoutDuplicate = occurrences.filter((value) => value !== occurrence);
return [...withoutDuplicate, occurrence].slice(-maxRememberedOccurrences);
};
export const claimBrowserAlertOccurrence = (storageKey: string, occurrence: string): boolean => {
if (typeof window === "undefined") return false;
try {
const serialized = window.localStorage.getItem(storageKey);
const parsed: unknown = serialized === null ? [] : JSON.parse(serialized);
const occurrences = Array.isArray(parsed)
? parsed.filter((value): value is string => typeof value === "string")
: [];
if (occurrences.includes(occurrence)) return false;
window.localStorage.setItem(storageKey, JSON.stringify(addRememberedBrowserAlert(occurrences, occurrence)));
return true;
} catch {
return false;
}
};