Add configurable download sound and improve cancellation retries

This commit is contained in:
Dariusz L
2026-10-05 19:49:23 +02:00
parent 561b57f2e6
commit 22f4cbcdee
8 changed files with 276 additions and 14 deletions
@@ -9,6 +9,7 @@ import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js
import { matchesLocalModelDownload } from "../utils/local_match_utils.js";
import { matchesSearchText } from "../utils/search_utils.js";
import { normalizeSourceKey } from "../utils/source_labels.js";
import { playDownloadSuccessSound } from "../../utils/download_sound.js";
const log = createModuleLogger('resolve_download_methods');
export const resolveDownloadMethods = {
@@ -2225,6 +2226,7 @@ export const resolveDownloadMethods = {
this.updateDownloadAllButtonState();
this.updateQueuePanel?.();
this.refreshLocalMatchesUiForMissing?.(missing);
if (!alreadyExists) playDownloadSuccessSound();
this.showNotification(alreadyExists
? `Already downloaded: ${progress.filename}`
: `Downloaded: ${progress.filename}`, 'success', {
@@ -2333,16 +2335,10 @@ export const resolveDownloadMethods = {
this.fetchJson(`/model_resolver/cancel/${downloadId}`, {
method: 'POST'
}, 'Cancel download').then(() => {
if (!info) return;
this.finalizeCancelledDownloadFrontend?.(downloadId, info, {
...(info.lastProgress || {}),
status: 'cancelled',
filename: info.lastProgress?.filename || info.filename || '',
path: info.lastProgress?.path || info.downloadPath || '',
directory: info.lastProgress?.directory || info.downloadDirectory || ''
});
this.showNotification('Download cancelled', 'info');
// The request only acknowledges cancellation. Progress polling
// releases the file and enables retry when the worker has stopped.
}).catch((error) => {
this.clearPendingDownloadStatus(info);
console.error('Model Resolver: Cancel error:', error);
this.showNotification('Failed to cancel download', 'error');
});
+12
View File
@@ -295,6 +295,15 @@ export const optionsMethods = {
${renderDownloadBackendOptions(tokens.download_backend)}
</select>
</div>
<label class="mr-options-toggle-row mr-options-compact-toggle-row">
<div class="mr-options-toggle-copy">
<span class="mr-options-toggle-title">Download sound notifications <span class="mr-tooltip-badge" data-tooltip="Play a sound when a model finishes downloading successfully.">?</span></span>
</div>
<span class="mr-options-toggle-control">
<input id="mr-options-download-sound" class="mr-options-switch-input" type="checkbox" ${tokens.download_sound_enabled !== false ? 'checked' : ''}>
<span class="mr-options-switch"></span>
</span>
</label>
<div class="mr-options-dependent-block mr-options-aria2-setting">
<div class="mr-options-number-row mr-options-wide-row">
<div class="mr-options-number-copy">
@@ -969,6 +978,7 @@ export const optionsMethods = {
const aria2AvailabilityEl = this.contentElement.querySelector('#mr-options-aria2-availability');
const aria2DaemonEl = this.contentElement.querySelector('#mr-options-aria2-daemon');
const aria2AutoStopInput = this.contentElement.querySelector('#mr-options-aria2-auto-stop');
const downloadSoundInput = this.contentElement.querySelector('#mr-options-download-sound');
const aria2SettingEls = Array.from(this.contentElement.querySelectorAll('.mr-options-aria2-setting'));
const downloadPathModeInput = this.contentElement.querySelector('#mr-options-download-path-mode');
const defaultRootSelectInputs = Array.from(this.contentElement.querySelectorAll('.mr-options-default-root'));
@@ -1091,6 +1101,7 @@ export const optionsMethods = {
downloadBackendInput,
aria2cPathInput,
aria2AutoStopInput,
downloadSoundInput,
downloadPathModeInput,
...defaultRootSelectInputs,
...templatePresetInputs,
@@ -3508,6 +3519,7 @@ export const optionsMethods = {
download_backend: downloadBackend,
aria2c_path: aria2cPathInput?.value?.trim() || '',
aria2_auto_stop_daemon: Boolean(aria2AutoStopInput?.checked),
download_sound_enabled: Boolean(downloadSoundInput?.checked),
download_path_mode: downloadPathMode,
download_path_templates: downloadPathTemplates,
base_model_path_mappings: baseModelPathMappings,
+53
View File
@@ -0,0 +1,53 @@
import { safeStorage } from '../resolver/utils/html_utils.js';
const notes = [
{ freq: 523, start: 0, duration: 0.27, volume: 0.055 },
{ freq: 659, start: 0.15, duration: 0.33, volume: 0.060 },
{ freq: 784, start: 0.30, duration: 0.48, volume: 0.055 },
{ freq: 1046, start: 0.47, duration: 0.40, volume: 0.022 },
];
let context;
function unlockAudio() {
try {
const AudioContext = globalThis.AudioContext || globalThis.webkitAudioContext;
if (!AudioContext) return;
context ||= new AudioContext();
if (context.state === 'suspended') void context.resume().catch(() => {});
} catch {
// Audio availability must not affect downloads.
}
}
// Browsers require a user gesture before allowing notification audio.
globalThis.document?.addEventListener('pointerdown', unlockAudio, { capture: true, passive: true });
globalThis.document?.addEventListener('keydown', unlockAudio, { capture: true, passive: true });
export function playDownloadSuccessSound() {
try {
if (safeStorage.getItem('ModelResolver.downloadSoundEnabled') === 'false' || context?.state !== 'running') return;
const now = context.currentTime;
for (const note of notes) {
const oscillator = context.createOscillator();
const gain = context.createGain();
const start = now + note.start;
const end = start + note.duration;
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(note.freq, start);
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(note.volume, start + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, end);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.onended = () => {
oscillator.disconnect();
gain.disconnect();
};
oscillator.start(start);
oscillator.stop(end + 0.02);
}
} catch {
// Notification audio is optional when the browser cannot play it.
}
}