diff --git a/src/models/messages.ts b/src/models/messages.ts index 51b22ba..59ae989 100644 --- a/src/models/messages.ts +++ b/src/models/messages.ts @@ -86,8 +86,7 @@ export interface ITestNotificationMessage extends IMessagable { } export interface IPlaySoundMessage extends IMessagable { - source: string; - volume: number; + isFailed: boolean; } interface IMessagable { diff --git a/src/notifications/offscreen.ts b/src/notifications/offscreen.ts index d9227c5..5e0f028 100644 --- a/src/notifications/offscreen.ts +++ b/src/notifications/offscreen.ts @@ -1,13 +1,33 @@ import { IPlaySoundMessage, PlaySoundMessage } from "../models/messages"; +import { getCustomSound, SoundKind } from "../util/sound-storage"; chrome.runtime.onMessage.addListener(message => { if (message.action === PlaySoundMessage.action) { - playAudio((message as IPlaySoundMessage).source); - } + playNotificationSound((message as IPlaySoundMessage).isFailed); + } }); -// Play sound with access to DOM APIs -function playAudio(source: string) { +// Play the user's custom sound for this outcome if one is stored, otherwise +// fall back to the bundled default. Runs in the offscreen document, which has +// DOM access (Audio, URL.createObjectURL) the service worker lacks. +async function playNotificationSound(isFailed: boolean): Promise { + const kind: SoundKind = isFailed ? "failure" : "success"; + + let source = `../assets/sounds/${kind}.ogg`; + let objectUrl: string | undefined; + try { + const custom = await getCustomSound(kind); + if (custom) { + objectUrl = URL.createObjectURL(custom.blob); + source = objectUrl; + } + } catch (e) { + console.error("Failed to load custom sound, using default", e); + } + const audio = new Audio(source); - audio.play(); + if (objectUrl) { + audio.addEventListener("ended", () => URL.revokeObjectURL(objectUrl!)); + } + audio.play().catch(e => console.error("Failed to play notification sound", e)); } diff --git a/src/options/pages/NotificationsPage.tsx b/src/options/pages/NotificationsPage.tsx index 9df7bd1..edd176d 100644 --- a/src/options/pages/NotificationsPage.tsx +++ b/src/options/pages/NotificationsPage.tsx @@ -1,73 +1,125 @@ +import { useEffect, useRef, useState, CSSProperties } from "react"; import { useSettings } from "../SettingsContext"; import { ITestNotificationMessage, TestNotificationMessage } from "../../models/messages"; +import { deleteCustomSound, getCustomSound, saveCustomSound, SoundKind } from "../../util/sound-storage"; import Toggle from "../components/Toggle"; +// Custom sounds are kept small; they live in IndexedDB but are still loaded +// into memory to play, so cap the size to keep things snappy. +const MAX_SOUND_BYTES = 1024 * 1024; // 1 MB + +const sectionStyle: CSSProperties = { + background: "var(--rta-surface-alt, #f7faf7)", + border: "1px solid var(--rta-border, #b7c9a7)", + borderRadius: 12, + padding: 18, + marginBottom: 18, +}; + +const sectionTitleStyle: CSSProperties = { + margin: "0 0 14px 0", + fontSize: 13, + fontWeight: 700, + letterSpacing: 0.6, + textTransform: "uppercase", + color: "var(--rta-green-dark, #4e6a57)", +}; + +const rowStyle: CSSProperties = { + display: "flex", + alignItems: "center", + gap: 12, + marginBottom: 14, +}; + +const buttonStyle: CSSProperties = { + border: "1px solid var(--rta-border, #b7c9a7)", + borderRadius: 8, + padding: "6px 14px", + background: "var(--rta-surface, #fff)", + color: "var(--rta-text, #1b241d)", + fontWeight: 500, + cursor: "pointer", +}; + +const inputStyle: CSSProperties = { + width: 110, + background: "var(--rta-input-bg, #fff)", + color: "var(--rta-text, #1b241d)", + border: "1px solid var(--rta-border, #b7c9a7)", + borderRadius: 8, + padding: "6px 10px", +}; + export default function NotificationsPage() { const { settings, updateSetting, loading } = useSettings(); if (loading || !settings) return
Loading...
; - const handleTestSuccess = () => { + const sendTest = (isFailed: boolean) => { chrome.runtime.sendMessage({ action: TestNotificationMessage.action, title: "Test Notification", - message: "This is a successful notification.", - isFailed: false, + message: isFailed ? "This is a failed notification." : "This is a successful notification.", + isFailed, popupDurationMs: settings.notificationsDurationMs, - playSound: settings.notificationsSoundEnabled + playSound: settings.notificationsSoundEnabled, } as ITestNotificationMessage); }; - const handleTestFail = () => { - chrome.runtime.sendMessage({ - action: TestNotificationMessage.action, - title: "Test Notification", - message: "This is a failed notification.", - isFailed: true, - popupDurationMs: settings.notificationsDurationMs, - playSound: settings.notificationsSoundEnabled - } as ITestNotificationMessage); - }; - - return ( -
-
- updateSetting("notificationsEnabled", v)} - label="Show notifications" - /> -
-
- updateSetting("notificationsSoundEnabled", v)} - label="Sound" - /> -
-
-