From 234b73c8a21460f38ef013e415faef6efab32909 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 13 Jul 2026 09:40:53 +0800 Subject: [PATCH] feat(ui): add cancel button to download progress modal --- locales/de.json | 4 +- locales/en.json | 4 +- locales/es.json | 4 +- locales/fr.json | 4 +- locales/he.json | 4 +- locales/ja.json | 4 +- locales/ko.json | 4 +- locales/ru.json | 4 +- locales/zh-CN.json | 4 +- locales/zh-TW.json | 4 +- static/js/api/baseModelApi.js | 12 ++ .../BulkMissingLoraDownloadManager.js | 33 +++++- static/js/managers/DownloadManager.js | 107 +++++++++++++++--- static/js/managers/LoadingManager.js | 4 + static/js/managers/import/DownloadManager.js | 37 ++++-- 15 files changed, 195 insertions(+), 38 deletions(-) diff --git a/locales/de.json b/locales/de.json index a428c70d..a0a6c4f5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1207,7 +1207,9 @@ "preparing": "Download wird vorbereitet...", "downloadedPreview": "Vorschaubild heruntergeladen", "downloadingFile": "{type}-Datei wird heruntergeladen", - "finalizing": "Download wird abgeschlossen..." + "finalizing": "Download wird abgeschlossen...", + "cancelling": "Download wird abgebrochen...", + "cancelled": "Download abgebrochen" }, "progress": { "currentFile": "Aktuelle Datei:", diff --git a/locales/en.json b/locales/en.json index df1526f6..9fb5ff8b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1207,7 +1207,9 @@ "preparing": "Preparing download...", "downloadedPreview": "Downloaded preview image", "downloadingFile": "Downloading {type} file", - "finalizing": "Finalizing download..." + "finalizing": "Finalizing download...", + "cancelling": "Cancelling download...", + "cancelled": "Download cancelled" }, "progress": { "currentFile": "Current file:", diff --git a/locales/es.json b/locales/es.json index 57ff5506..4f47a6e0 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1207,7 +1207,9 @@ "preparing": "Preparando descarga...", "downloadedPreview": "Imagen de vista previa descargada", "downloadingFile": "Descargando archivo de {type}", - "finalizing": "Finalizando descarga..." + "finalizing": "Finalizando descarga...", + "cancelling": "Cancelando descarga...", + "cancelled": "Descarga cancelada" }, "progress": { "currentFile": "Archivo actual:", diff --git a/locales/fr.json b/locales/fr.json index 8bbcd691..0a84a599 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1207,7 +1207,9 @@ "preparing": "Préparation du téléchargement...", "downloadedPreview": "Image d'aperçu téléchargée", "downloadingFile": "Téléchargement du fichier {type}", - "finalizing": "Finalisation du téléchargement..." + "finalizing": "Finalisation du téléchargement...", + "cancelling": "Annulation du téléchargement...", + "cancelled": "Téléchargement annulé" }, "progress": { "currentFile": "Fichier actuel :", diff --git a/locales/he.json b/locales/he.json index 6903c380..d4ff69ee 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1207,7 +1207,9 @@ "preparing": "מכין הורדה...", "downloadedPreview": "תמונת תצוגה מקדימה הורדה", "downloadingFile": "מוריד קובץ {type}", - "finalizing": "מסיים הורדה..." + "finalizing": "מסיים הורדה...", + "cancelling": "מבטל הורדה...", + "cancelled": "ההורדה בוטלה" }, "progress": { "currentFile": "הקובץ הנוכחי:", diff --git a/locales/ja.json b/locales/ja.json index a3930b18..0267476c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1207,7 +1207,9 @@ "preparing": "ダウンロードを準備中...", "downloadedPreview": "プレビュー画像をダウンロードしました", "downloadingFile": "{type}ファイルをダウンロード中", - "finalizing": "ダウンロードを完了中..." + "finalizing": "ダウンロードを完了中...", + "cancelling": "ダウンロードをキャンセル中...", + "cancelled": "ダウンロードをキャンセルしました" }, "progress": { "currentFile": "現在のファイル:", diff --git a/locales/ko.json b/locales/ko.json index ded25df2..4c8cc294 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1207,7 +1207,9 @@ "preparing": "다운로드 준비 중...", "downloadedPreview": "미리보기 이미지 다운로드됨", "downloadingFile": "{type} 파일 다운로드 중", - "finalizing": "다운로드 완료 중..." + "finalizing": "다운로드 완료 중...", + "cancelling": "다운로드 취소 중...", + "cancelled": "다운로드가 취소되었습니다" }, "progress": { "currentFile": "현재 파일:", diff --git a/locales/ru.json b/locales/ru.json index 1dc9050a..620eb5bc 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1207,7 +1207,9 @@ "preparing": "Подготовка загрузки...", "downloadedPreview": "Превью изображение загружено", "downloadingFile": "Загрузка файла {type}", - "finalizing": "Завершение загрузки..." + "finalizing": "Завершение загрузки...", + "cancelling": "Отмена загрузки...", + "cancelled": "Загрузка отменена" }, "progress": { "currentFile": "Текущий файл:", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index c7385b71..61bf74ac 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1207,7 +1207,9 @@ "preparing": "正在准备下载...", "downloadedPreview": "预览图片已下载", "downloadingFile": "正在下载 {type} 文件", - "finalizing": "正在完成下载..." + "finalizing": "正在完成下载...", + "cancelling": "取消下载中...", + "cancelled": "下载已取消" }, "progress": { "currentFile": "当前文件:", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f3828223..a8a5b5e2 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1207,7 +1207,9 @@ "preparing": "準備下載中...", "downloadedPreview": "已下載預覽圖片", "downloadingFile": "正在下載 {type} 檔案", - "finalizing": "完成下載中..." + "finalizing": "完成下載中...", + "cancelling": "取消下載中...", + "cancelled": "下載已取消" }, "progress": { "currentFile": "目前檔案:", diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 0fa4b562..0ff81913 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -112,6 +112,18 @@ export class BaseModelApiClient { } } + async cancelDownload(downloadId) { + try { + const response = await fetch( + `${DOWNLOAD_ENDPOINTS.cancelGet}?download_id=${encodeURIComponent(downloadId)}` + ); + return await response.json(); + } catch (error) { + console.error('Error cancelling download:', error); + return { success: false, error: error.message }; + } + } + async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) { const pageState = this.getPageState(); diff --git a/static/js/managers/BulkMissingLoraDownloadManager.js b/static/js/managers/BulkMissingLoraDownloadManager.js index e73638b2..baa8359a 100644 --- a/static/js/managers/BulkMissingLoraDownloadManager.js +++ b/static/js/managers/BulkMissingLoraDownloadManager.js @@ -196,6 +196,17 @@ export class BulkMissingLoraDownloadManager { let completedDownloads = 0; let failedDownloads = 0; let currentLoraProgress = 0; + let cancelled = false; + + loadingManager.showCancelButton(async () => { + if (cancelled) return; + cancelled = true; + try { + await this.loraApiClient.cancelDownload(batchDownloadId); + } catch (e) { + console.error('Cancel request failed:', e); + } + }); // Set up WebSocket message handler ws.onmessage = (event) => { @@ -207,6 +218,11 @@ export class BulkMissingLoraDownloadManager { return; } + if (data.status === 'cancelled') { + cancelled = true; + return; + } + // Process progress updates if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) { currentLoraProgress = data.progress; @@ -249,6 +265,8 @@ export class BulkMissingLoraDownloadManager { // Download each LoRA sequentially for (let i = 0; i < lorasToDownload.length; i++) { + if (cancelled) break; + const lora = lorasToDownload[i]; currentLoraProgress = 0; @@ -275,11 +293,13 @@ export class BulkMissingLoraDownloadManager { modelId, versionId, loraRoot, - '', // Empty relative path, use default paths + '', useDefaultPaths, batchDownloadId ); + if (cancelled) break; + if (!response.success) { console.error(`Failed to download LoRA ${lora.name || lora.file_name}: ${response.error}`); failedDownloads++; @@ -288,8 +308,10 @@ export class BulkMissingLoraDownloadManager { updateProgress(100, completedDownloads, ''); } } catch (error) { - console.error(`Error downloading LoRA ${lora.name || lora.file_name}:`, error); - failedDownloads++; + if (!cancelled) { + console.error(`Error downloading LoRA ${lora.name || lora.file_name}:`, error); + failedDownloads++; + } } } @@ -300,7 +322,10 @@ export class BulkMissingLoraDownloadManager { loadingManager.hide(); // Show completion message - if (failedDownloads === 0) { + if (cancelled) { + showToast('toast.downloads.downloadStopped', {}, 'info', + `Download cancelled. ${completedDownloads} item(s) completed.`); + } else if (failedDownloads === 0) { showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success'); } else { showToast('toast.loras.downloadPartialSuccess', { diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 674f4a43..a4fddbd8 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -872,6 +872,7 @@ export class DownloadManager { const displayName = versionName || `#${versionId}`; let ws = null; let updateProgress = () => { }; + let cancelled = false; try { this.loadingManager.restoreProgressBar(); @@ -882,6 +883,16 @@ export class DownloadManager { const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`); + this.loadingManager.showCancelButton(async () => { + if (cancelled) return; + cancelled = true; + try { + await this.apiClient.cancelDownload(downloadId); + } catch (e) { + console.error('Cancel request failed:', e); + } + }); + ws.onmessage = event => { const data = JSON.parse(event.data); @@ -890,6 +901,12 @@ export class DownloadManager { return; } + if (data.status === 'cancelled') { + cancelled = true; + this.loadingManager.setStatus(translate('modals.download.status.cancelled', {}, 'Download cancelled')); + return; + } + if (data.status === 'progress' && data.download_id === downloadId) { const metrics = { bytesDownloaded: data.bytes_downloaded, @@ -928,6 +945,10 @@ export class DownloadManager { fileParams ); + if (cancelled) { + return false; + } + if (response?.skipped) { this.loadingManager.setStatus(translate('modals.download.status.finalizing')); updateProgress(100, 0, displayName); @@ -968,8 +989,12 @@ export class DownloadManager { return true; } catch (error) { - console.error('Failed to download model version:', error); - showToast('toast.downloads.downloadError', { message: error?.message }, 'error'); + if (cancelled) { + console.log('Download cancelled by user:', downloadId); + } else { + console.error('Failed to download model version:', error); + showToast('toast.downloads.downloadError', { message: error?.message }, 'error'); + } return false; } finally { try { @@ -989,16 +1014,33 @@ export class DownloadManager { const totalFiles = this.hfSelectedFiles.length; const updateProgress = this.loadingManager.showDownloadProgress(totalFiles); + let cancelled = false; + let currentDownloadId = null; + + this.loadingManager.showCancelButton(async () => { + if (cancelled) return; + cancelled = true; + if (currentDownloadId) { + try { + await this.apiClient.cancelDownload(currentDownloadId); + } catch (e) { + console.error('Cancel request failed:', e); + } + } + }); + try { let completedDownloads = 0; for (let i = 0; i < totalFiles; i++) { + if (cancelled) break; + const filename = this.hfSelectedFiles[i]; updateProgress(0, completedDownloads, filename); this.loadingManager.setStatus(`Downloading ${filename}...`); - const downloadId = Date.now().toString() + '_' + i; + currentDownloadId = Date.now().toString() + '_' + i; const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`); + const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${currentDownloadId}`); try { await new Promise((resolve, reject) => { @@ -1006,12 +1048,13 @@ export class DownloadManager { ws.onerror = reject; }); - // Capture completed count at WS creation time so progress - // updates arriving after completedDownloads increments still - // show the correct "N / total" position. const snapshotCompleted = completedDownloads; ws.onmessage = (event) => { const data = JSON.parse(event.data); + if (data.status === 'cancelled') { + cancelled = true; + return; + } if (data.status === 'progress') { const metrics = { bytesDownloaded: data.bytes_downloaded, @@ -1029,9 +1072,11 @@ export class DownloadManager { modelRoot, relativePath: targetFolder, useDefaultPaths, - download_id: downloadId, + download_id: currentDownloadId, }); + if (cancelled) break; + if (response?.success) { completedDownloads++; updateProgress(100, completedDownloads, filename); @@ -1041,13 +1086,19 @@ export class DownloadManager { } } - showToast('toast.loras.downloadCompleted', {}, 'success'); - // Reload page data — model is already in scanner cache via backend + if (cancelled) { + showToast('toast.downloads.downloadStopped', {}, 'info', + `Download cancelled. ${completedDownloads} item(s) completed.`); + } else { + showToast('toast.loras.downloadCompleted', {}, 'success'); + } await resetAndReload(true); return true; } catch (error) { - console.error('Failed to download HF model:', error); - showToast('toast.downloads.downloadError', { message: error?.message }, 'error'); + if (!cancelled) { + console.error('Failed to download HF model:', error); + showToast('toast.downloads.downloadError', { message: error?.message }, 'error'); + } return false; } finally { this.loadingManager.hide(); @@ -1470,11 +1521,27 @@ export class DownloadManager { let completedDownloads = 0; let failedDownloads = 0; + let cancelled = false; + + loadingManager.showCancelButton(async () => { + if (cancelled) return; + cancelled = true; + try { + await this.apiClient.cancelDownload(batchDownloadId); + } catch (e) { + console.error('Cancel request failed:', e); + } + }); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'download_id') return; + if (data.status === 'cancelled') { + cancelled = true; + return; + } + if (data.status === 'progress' && data.download_id?.startsWith(batchDownloadId)) { const current = downloadItems[completedDownloads + failedDownloads]; const name = current?.selectedVersion?.name || current?.displayName || current?.filename || `#${completedDownloads + failedDownloads + 1}`; @@ -1493,6 +1560,8 @@ export class DownloadManager { }); for (let i = 0; i < downloadItems.length; i++) { + if (cancelled) break; + const item = downloadItems[i]; const name = item.displayName || item.filename || (item.selectedVersion?.name || `Model #${item.modelId}`); const isHf = item.source === 'huggingface'; @@ -1503,7 +1572,6 @@ export class DownloadManager { try { let response; if (isHf) { - // Per-file WebSocket for real-time progress const downloadId = Date.now().toString() + '_hf_' + i; const wsHf = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`); try { @@ -1548,6 +1616,8 @@ export class DownloadManager { ); } + if (cancelled) break; + if (!response.success) { failedDownloads++; } else { @@ -1555,15 +1625,20 @@ export class DownloadManager { updateProgress(100, completedDownloads, ''); } } catch (err) { - console.error(`Failed to download ${name}:`, err); - failedDownloads++; + if (!cancelled) { + console.error(`Failed to download ${name}:`, err); + failedDownloads++; + } } } ws.close(); loadingManager.hide(); - if (failedDownloads === 0) { + if (cancelled) { + showToast('toast.downloads.downloadStopped', {}, 'info', + `Download cancelled. ${completedDownloads} item(s) completed.`); + } else if (failedDownloads === 0) { showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success'); } else { showToast('toast.loras.downloadPartialSuccess', { diff --git a/static/js/managers/LoadingManager.js b/static/js/managers/LoadingManager.js index 1a53d7e1..36533aa9 100644 --- a/static/js/managers/LoadingManager.js +++ b/static/js/managers/LoadingManager.js @@ -281,6 +281,10 @@ export class LoadingManager { // Initialize transfer stats with empty data updateTransferStats(); + if (this.cancelButton) { + this.loadingContent.appendChild(this.cancelButton); + } + // Return update function return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => { // Update current item progress diff --git a/static/js/managers/import/DownloadManager.js b/static/js/managers/import/DownloadManager.js index 7dbb51e1..9d2f4c31 100644 --- a/static/js/managers/import/DownloadManager.js +++ b/static/js/managers/import/DownloadManager.js @@ -168,6 +168,18 @@ export class DownloadManager { let failedDownloads = 0; let accessFailures = 0; let currentLoraProgress = 0; + let cancelled = false; + + this.importManager.loadingManager.showCancelButton(async () => { + if (cancelled) return; + cancelled = true; + try { + const loraClient = getModelApiClient(MODEL_TYPES.LORA); + await loraClient.cancelDownload(batchDownloadId); + } catch (e) { + console.error('Cancel request failed:', e); + } + }); // Set up progress tracking for current download ws.onmessage = (event) => { @@ -179,6 +191,11 @@ export class DownloadManager { return; } + if (data.status === 'cancelled') { + cancelled = true; + return; + } + // Process progress updates for our current active download if (data.status === 'progress' && data.download_id && data.download_id.startsWith(batchDownloadId)) { // Update current LoRA progress @@ -221,6 +238,8 @@ export class DownloadManager { const useDefaultPaths = getStorageItem('use_default_path_loras', false); for (let i = 0; i < this.importManager.downloadableLoRAs.length; i++) { + if (cancelled) break; + const lora = this.importManager.downloadableLoRAs[i]; // Reset current LoRA progress for new download @@ -241,15 +260,13 @@ export class DownloadManager { batchDownloadId ); + if (cancelled) break; + if (!response.success) { console.error(`Failed to download LoRA ${lora.name}: ${response.error}`); - failedDownloads++; - // Continue with next download } else { completedDownloads++; - - // Update progress to show completion of current LoRA updateProgress(100, completedDownloads, ''); if (completedDownloads + failedDownloads < this.importManager.downloadableLoRAs.length) { @@ -259,9 +276,10 @@ export class DownloadManager { } } } catch (downloadError) { - console.error(`Error downloading LoRA ${lora.name}:`, downloadError); - failedDownloads++; - // Continue with next download + if (!cancelled) { + console.error(`Error downloading LoRA ${lora.name}:`, downloadError); + failedDownloads++; + } } } @@ -269,7 +287,10 @@ export class DownloadManager { ws.close(); // Show appropriate completion message based on results - if (failedDownloads === 0) { + if (cancelled) { + showToast('toast.downloads.downloadStopped', {}, 'info', + `Download cancelled. ${completedDownloads} item(s) completed.`); + } else if (failedDownloads === 0) { showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success'); } else { if (accessFailures > 0) {