feat(ui): add cancel button to download progress modal

This commit is contained in:
Will Miao
2026-07-13 09:40:53 +08:00
parent abd06c48f4
commit 234b73c8a2
15 changed files with 195 additions and 38 deletions

View File

@@ -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:",

View File

@@ -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:",

View File

@@ -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:",

View File

@@ -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 :",

View File

@@ -1207,7 +1207,9 @@
"preparing": "מכין הורדה...",
"downloadedPreview": "תמונת תצוגה מקדימה הורדה",
"downloadingFile": "מוריד קובץ {type}",
"finalizing": "מסיים הורדה..."
"finalizing": "מסיים הורדה...",
"cancelling": "מבטל הורדה...",
"cancelled": "ההורדה בוטלה"
},
"progress": {
"currentFile": "הקובץ הנוכחי:",

View File

@@ -1207,7 +1207,9 @@
"preparing": "ダウンロードを準備中...",
"downloadedPreview": "プレビュー画像をダウンロードしました",
"downloadingFile": "{type}ファイルをダウンロード中",
"finalizing": "ダウンロードを完了中..."
"finalizing": "ダウンロードを完了中...",
"cancelling": "ダウンロードをキャンセル中...",
"cancelled": "ダウンロードをキャンセルしました"
},
"progress": {
"currentFile": "現在のファイル:",

View File

@@ -1207,7 +1207,9 @@
"preparing": "다운로드 준비 중...",
"downloadedPreview": "미리보기 이미지 다운로드됨",
"downloadingFile": "{type} 파일 다운로드 중",
"finalizing": "다운로드 완료 중..."
"finalizing": "다운로드 완료 중...",
"cancelling": "다운로드 취소 중...",
"cancelled": "다운로드가 취소되었습니다"
},
"progress": {
"currentFile": "현재 파일:",

View File

@@ -1207,7 +1207,9 @@
"preparing": "Подготовка загрузки...",
"downloadedPreview": "Превью изображение загружено",
"downloadingFile": "Загрузка файла {type}",
"finalizing": "Завершение загрузки..."
"finalizing": "Завершение загрузки...",
"cancelling": "Отмена загрузки...",
"cancelled": "Загрузка отменена"
},
"progress": {
"currentFile": "Текущий файл:",

View File

@@ -1207,7 +1207,9 @@
"preparing": "正在准备下载...",
"downloadedPreview": "预览图片已下载",
"downloadingFile": "正在下载 {type} 文件",
"finalizing": "正在完成下载..."
"finalizing": "正在完成下载...",
"cancelling": "取消下载中...",
"cancelled": "下载已取消"
},
"progress": {
"currentFile": "当前文件:",

View File

@@ -1207,7 +1207,9 @@
"preparing": "準備下載中...",
"downloadedPreview": "已下載預覽圖片",
"downloadingFile": "正在下載 {type} 檔案",
"finalizing": "完成下載中..."
"finalizing": "完成下載中...",
"cancelling": "取消下載中...",
"cancelled": "下載已取消"
},
"progress": {
"currentFile": "目前檔案:",

View File

@@ -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();

View File

@@ -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', {

View File

@@ -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', {

View File

@@ -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

View File

@@ -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) {