fix(app): decouple completion chime

Play the completion sound through the app with Web Audio so it is not tied to whether desktop notifications are enabled or whether a platform-specific notification sound name is supported.

Keep the notification body focused on completion status and leave the settings toggle available even when notifications are disabled.
This commit is contained in:
NimBold
2026-07-02 01:58:43 +03:30
parent 1e61d05873
commit 276446a4dd
2 changed files with 35 additions and 30 deletions
+34 -15
View File
@@ -42,9 +42,34 @@ const getScheduledQueueIds = () => {
return selectedQueueIds;
};
type AudioContextConstructor = typeof AudioContext;
const playCompletionChime = () => {
const AudioCtor =
window.AudioContext ||
(window as Window & { webkitAudioContext?: AudioContextConstructor }).webkitAudioContext;
if (!AudioCtor) return;
const context = new AudioCtor();
const oscillator = context.createOscillator();
const gain = context.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, context.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(1320, context.currentTime + 0.12);
gain.gain.setValueAtTime(0.0001, context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.18, context.currentTime + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.24);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start();
oscillator.stop(context.currentTime + 0.24);
oscillator.onended = () => {
void context.close();
};
};
function App() {
const platform = usePlatformInfo();
const platformOsRef = useRef(platform.os);
const [filter, setFilter] = useState<SidebarFilter>('all');
const [coreReady, setCoreReady] = useState(false);
@@ -115,10 +140,6 @@ function App() {
const { addToast } = useToast();
useEffect(() => {
platformOsRef.current = platform.os;
}, [platform.os]);
useEffect(() => {
let active = true;
const initialize = async () => {
@@ -520,24 +541,22 @@ function App() {
const unlistenTerminalState = listen('download-state', (event) => {
if (event.payload.status !== 'completed' && event.payload.status !== 'failed') return;
const settings = useSettingsStore.getState();
if (event.payload.status === 'completed' && settings.playCompletionSound) {
try {
playCompletionChime();
} catch (error) {
console.error('Completion sound failed:', error);
}
}
if (!settings.showNotifications) return;
const item = useDownloadStore.getState().downloads.find(d => d.id === event.payload.id);
const fileName = item?.fileName || 'A file';
const platformOs = platformOsRef.current;
const sound = settings.playCompletionSound
? platformOs === 'macos'
? 'Ping'
: platformOs === 'linux'
? 'message-new-instant'
: undefined
: undefined;
if (event.payload.status === 'completed') {
try {
sendNotification({
title: 'Download Complete',
body: `${fileName} has finished downloading.`,
sound
body: `${fileName} has finished downloading.`
});
} catch (error) {
console.error('Completion notification failed:', error);
+1 -15
View File
@@ -526,19 +526,6 @@ runEngineChecks(false);
className="app-control w-24 text-center"
/>
</div>
<div className="mac-settings-row">
<div className="settings-row-label">
<span>Global speed limit:</span>
<small>{settings.globalSpeedLimit || 'Unlimited'}</small>
</div>
<button
type="button"
onClick={() => settings.setActiveView('speedLimiter')}
className="app-button px-3 text-xs"
>
Configure
</button>
</div>
<div className="mac-settings-row">
<div className="settings-row-label">
<span>Automatic retries:</span>
@@ -571,12 +558,11 @@ runEngineChecks(false);
className="mac-switch"
/>
</label>
<label className="mac-settings-row cursor-default" style={{ opacity: settings.showNotifications ? 1 : 0.5 }}>
<label className="mac-settings-row cursor-default">
<span className="text-[13px] text-text-primary">Play sound when download completes</span>
<input
type="checkbox"
checked={settings.playCompletionSound}
disabled={!settings.showNotifications}
onChange={(e) => settings.setPlayCompletionSound(e.target.checked)}
className="mac-switch"
/>