Add configurable download sound and improve cancellation retries
This commit is contained in:
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user