From 7cf785b72f2d3eecd056aadee65089539ed4f22b Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 30 Jun 2026 23:28:35 +0800 Subject: [PATCH] fix(ui): unify HF file selection UI, remove cloud icon, add select-all, cleanup dead code (#965, #977) - Unify single-URL and multi-URL HF repo flows to use the same batch preview interface (remove separate repoFileStep) - Remove unnecessary cloud icon from HF batch preview items - Use formatFileSize() instead of hardcoded MB text - Change default selection to unchecked (no preselected files) - Add select all / deselect all checkbox with dynamic Next button - Clean up dead CSS, HTML template, and JS methods from removed repoFileStep - Add selectAll i18n key with translations for all 10 locales - Fix batch progress bar name fallback for HF items --- locales/de.json | 1 + locales/en.json | 1 + locales/es.json | 1 + locales/fr.json | 1 + locales/he.json | 1 + locales/ja.json | 1 + locales/ko.json | 1 + locales/ru.json | 1 + locales/zh-CN.json | 1 + locales/zh-TW.json | 1 + .../css/components/modal/download-modal.css | 124 +++++---------- static/js/managers/DownloadManager.js | 150 ++++++++++-------- .../components/modals/download_modal.html | 18 --- 13 files changed, 130 insertions(+), 172 deletions(-) diff --git a/locales/de.json b/locales/de.json index d30b73d1..0dd96faf 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "Geben Sie eine CivitAI-, CivArchive- oder Hugging Face-URL pro Zeile ein. Unterstützt mehrere URLs für den Batch-Download.", "selectHfFiles": "Datei(en) zum Herunterladen aus diesem Repository auswählen:", + "selectAll": "Alle auswählen", "fetchingRepoFiles": "Repository-Dateien werden abgerufen...", "locationPreview": "Download-Speicherort Vorschau", "useDefaultPath": "Standardpfad verwenden", diff --git a/locales/en.json b/locales/en.json index 7fdf1811..6b481e0f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "Enter one CivitAI, CivArchive, or Hugging Face URL per line. Supports multiple URLs for batch download.", "selectHfFiles": "Select file(s) to download from this repository:", + "selectAll": "Select All", "fetchingRepoFiles": "Fetching repository files...", "locationPreview": "Download Location Preview", "useDefaultPath": "Use Default Path", diff --git a/locales/es.json b/locales/es.json index 41ad3a21..c2587398 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "Ingrese una URL de CivitAI, CivArchive o Hugging Face por línea. Admite múltiples URLs para descarga por lotes.", "selectHfFiles": "Seleccione el/los archivo(s) para descargar de este repositorio:", + "selectAll": "Seleccionar todo", "fetchingRepoFiles": "Obteniendo archivos del repositorio...", "locationPreview": "Vista previa de ubicación de descarga", "useDefaultPath": "Usar ruta predeterminada", diff --git a/locales/fr.json b/locales/fr.json index 09ed3837..e461f4fb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "Entrez une URL CivitAI, CivArchive ou Hugging Face par ligne. Prend en charge plusieurs URL pour le téléchargement par lot.", "selectHfFiles": "Sélectionnez le(s) fichier(s) à télécharger depuis ce dépôt :", + "selectAll": "Tout sélectionner", "fetchingRepoFiles": "Récupération des fichiers du dépôt...", "locationPreview": "Aperçu de l'emplacement de téléchargement", "useDefaultPath": "Utiliser le chemin par défaut", diff --git a/locales/he.json b/locales/he.json index ee4c42c3..584f7ecd 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "יש להזין כתובת URL אחת של CivitAI, CivArchive או Hugging Face בכל שורה. תומך במספר כתובות URL להורדה בקבוצה.", "selectHfFiles": "בחר קבצים להורדה ממאגר זה:", + "selectAll": "בחר הכל", "fetchingRepoFiles": "מביא קבצים מהמאגר...", "locationPreview": "תצוגה מקדימה של מיקום ההורדה", "useDefaultPath": "השתמש בנתיב ברירת מחדל", diff --git a/locales/ja.json b/locales/ja.json index b57bcd67..8a44591b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "1行に1つのCivitAI、CivArchive、またはHugging Face URLを入力してください。複数のURLを一括ダウンロードできます。", "selectHfFiles": "このリポジトリからダウンロードするファイルを選択してください:", + "selectAll": "すべて選択", "fetchingRepoFiles": "リポジトリのファイルを取得中...", "locationPreview": "ダウンロード場所プレビュー", "useDefaultPath": "デフォルトパスを使用", diff --git a/locales/ko.json b/locales/ko.json index cc16dea5..293c3e21 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "한 줄에 하나의 CivitAI, CivArchive 또는 Hugging Face URL을 입력하세요. 여러 URL을 일괄 다운로드할 수 있습니다.", "selectHfFiles": "이 저장소에서 다운로드할 파일을 선택하세요:", + "selectAll": "모두 선택", "fetchingRepoFiles": "저장소 파일을 가져오는 중...", "locationPreview": "다운로드 위치 미리보기", "useDefaultPath": "기본 경로 사용", diff --git a/locales/ru.json b/locales/ru.json index 527d02ab..4d023ac9 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "Введите один URL CivitAI, CivArchive или Hugging Face в каждой строке. Поддерживает несколько URL для пакетной загрузки.", "selectHfFiles": "Выберите файл(ы) для загрузки из этого репозитория:", + "selectAll": "Выбрать все", "fetchingRepoFiles": "Получение файлов репозитория...", "locationPreview": "Предпросмотр места загрузки", "useDefaultPath": "Использовать путь по умолчанию", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 49a790c0..9ccc1bb4 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "每行输入一个 CivitAI、CivArchive 或 Hugging Face URL。支持批量下载多个 URL。", "selectHfFiles": "选择从此仓库下载的文件:", + "selectAll": "全选", "fetchingRepoFiles": "正在获取仓库文件...", "locationPreview": "下载位置预览", "useDefaultPath": "使用默认路径", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index aea3500e..ade31976 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1136,6 +1136,7 @@ "placeholder": "https://civitai.com/models/...", "urlHint": "每行輸入一個 CivitAI、CivArchive 或 Hugging Face URL。支援批量下載多個 URL。", "selectHfFiles": "選擇從此倉庫下載的檔案:", + "selectAll": "全選", "fetchingRepoFiles": "正在獲取倉庫檔案...", "locationPreview": "下載位置預覽", "useDefaultPath": "使用預設路徑", diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index 88e5c31d..f0975456 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -823,87 +823,6 @@ background: var(--lora-surface); } -/* HF Repo File Explorer Step */ -.hf-repo-header { - margin-bottom: var(--space-2); - font-size: 0.95em; - color: var(--text-color); - opacity: 0.8; -} - -.repo-file-list { - max-height: 360px; - overflow-y: auto; - margin: var(--space-2) 0; - display: flex; - flex-direction: column; - gap: 6px; -} - -.repo-file-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - cursor: pointer; - transition: var(--transition-base); - background: var(--bg-color); -} - -.repo-file-item:hover { - border-color: var(--lora-accent); - box-shadow: var(--shadow-sm); -} - -.repo-file-item.selected { - border: 2px solid var(--lora-accent); - background: oklch(var(--lora-accent) / 0.05); -} - -.repo-file-item .repo-file-checkbox { - width: 18px; - height: 18px; - cursor: pointer; - accent-color: var(--lora-accent); - flex-shrink: 0; - padding: 0; - border: none; -} - -.repo-file-icon { - font-size: 1.2em; - color: var(--text-color); - opacity: 0.6; - width: 24px; - text-align: center; - flex-shrink: 0; -} - -.repo-file-name { - flex: 1; - font-weight: 500; - font-size: 0.95em; - word-break: keep-all; - overflow-wrap: anywhere; - min-width: 0; -} - -.repo-file-meta { - display: flex; - align-items: center; - gap: 8px; - font-size: 0.85em; - color: var(--text-color); - opacity: 0.6; - white-space: nowrap; -} - -.repo-file-size { - font-variant-numeric: tabular-nums; -} - .hf-badge { display: inline-block; padding: 1px 6px; @@ -915,9 +834,6 @@ margin-left: 4px; } -[data-theme="dark"] .repo-file-item { - background: var(--lora-surface); -} /* Checkbox inside HF batch preview items */ .batch-preview-checkbox { @@ -929,4 +845,42 @@ padding: 0; border: none; margin: 0; -} \ No newline at end of file +} + +/* Select All toolbar in batch preview */ +.batch-preview-select-all { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); + background: var(--lora-surface); + cursor: pointer; + position: sticky; + top: 0; + z-index: 1; +} + +.batch-preview-select-all input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--lora-accent); + flex-shrink: 0; + padding: 0; + border: none; + margin: 0; +} + +.batch-preview-select-all label { + cursor: pointer; + font-size: 0.9em; + color: var(--text-color); + font-weight: 500; + margin: 0; + user-select: none; +} + +[data-theme="dark"] .batch-preview-select-all { + background: var(--lora-surface); +} diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 8db246ac..7f2e942e 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -7,6 +7,7 @@ import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; import { FolderTreeManager } from '../components/FolderTreeManager.js'; import { translate } from '../utils/i18nHelpers.js'; import { extractCivitaiModelUrlParts } from '../utils/civitaiUtils.js'; +import { formatFileSize } from '../utils/formatters.js'; export class DownloadManager { constructor() { @@ -29,7 +30,6 @@ export class DownloadManager { // HF download state this.hfRepoId = null; - this.hfRepoFiles = []; this.hfSelectedFiles = []; this.loadingManager = new LoadingManager(); @@ -50,9 +50,7 @@ export class DownloadManager { this.handleBackToUrlFromBatch = this.backToUrlFromBatch.bind(this); this.handleNextFromBatch = this.nextFromBatch.bind(this); - // HF handlers - this.handleBackToUrlFromHf = this.backToUrlFromHf.bind(this); - this.handleNextFromHfFiles = this.nextFromHfFiles.bind(this); + } showDownloadModal() { @@ -109,11 +107,7 @@ export class DownloadManager { // Default path toggle handler document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); - // HF step buttons - const backToUrlFromHfBtn = document.getElementById('backToUrlFromHfBtn'); - if (backToUrlFromHfBtn) backToUrlFromHfBtn.addEventListener('click', this.handleBackToUrlFromHf); - const nextFromHfFiles = document.getElementById('nextFromHfFiles'); - if (nextFromHfFiles) nextFromHfFiles.addEventListener('click', this.handleNextFromHfFiles); + } updateModalLabels() { @@ -178,7 +172,6 @@ export class DownloadManager { // Reset HF state this.hfRepoId = null; - this.hfRepoFiles = []; this.hfSelectedFiles = []; } @@ -324,24 +317,36 @@ export class DownloadManager { this.isBatchMode = false; this.hfRepoId = info.repo; this.hfSelectedFiles = [info.filename]; - this.hfRepoFiles = []; this.source = 'huggingface'; this.proceedToLocation(); return; } - // Repo URL → fetch file list + // Repo URL → fetch file list and convert to batch items try { this.loadingManager.showSimpleLoading(translate('modals.download.fetchingRepoFiles')); const files = await this.apiClient.fetchHfRepoFiles(info.repo); if (!files || files.length === 0) { throw new Error(translate('modals.download.errors.noModelFiles')); } - this.hfRepoId = info.repo; - this.hfRepoFiles = files; - this.hfSelectedFiles = []; - this.isBatchMode = false; + this.isBatchMode = true; + this.batchModels = []; this.source = 'huggingface'; - this.showRepoFileStep(info.repo); + for (const file of files) { + this.batchModels.push({ + url: urls[0], + source: 'huggingface', + repo: info.repo, + filename: file.filename, + revision: 'main', + displayName: file.filename, + fileSizeBytes: file.size, + selectedVersion: true, + versions: [], + checked: false, + error: null, + }); + } + this.showBatchPreviewStep(); } catch (err) { errorElement.textContent = err.message; } finally { @@ -372,7 +377,7 @@ export class DownloadManager { displayName: info.filename, selectedVersion: true, versions: [], - checked: true, + checked: false, error: null, }); } else if (info.type === 'hf-repo') { @@ -394,7 +399,7 @@ export class DownloadManager { fileSizeBytes: file.size, selectedVersion: true, versions: [], - checked: true, + checked: false, error: null, }); } @@ -408,48 +413,6 @@ export class DownloadManager { this.showBatchPreviewStep(); } - showRepoFileStep(repoId) { - document.querySelectorAll('.download-step').forEach(s => s.style.display = 'none'); - document.getElementById('repoFileStep').style.display = 'block'; - document.getElementById('hfRepoLabel').textContent = repoId; - - const list = document.getElementById('repoFileList'); - list.innerHTML = this.hfRepoFiles.map((f, i) => { - const sizeMb = f.size > 0 ? (f.size / (1024 * 1024)).toFixed(1) : '?'; - return ` -
- - - ${f.filename} - - ${sizeMb} MB - -
- `; - }).join(''); - } - - backToUrlFromHf() { - this.hfRepoId = null; - this.hfRepoFiles = []; - this.hfSelectedFiles = []; - document.getElementById('repoFileStep').style.display = 'none'; - document.getElementById('urlStep').style.display = 'block'; - } - - nextFromHfFiles() { - // Read checked state directly from DOM — more reliable than event-tracking - const checked = document.querySelectorAll('.repo-file-checkbox:checked'); - this.hfSelectedFiles = Array.from(checked).map(cb => { - const idx = parseInt(cb.dataset.index); - return this.hfRepoFiles[idx].filename; - }); - if (!this.hfSelectedFiles.length) { - return; - } - this.proceedToLocation(); - } - async fetchVersionsForCurrentModel() { const errorElement = document.getElementById('urlError'); if (errorElement) { @@ -1114,7 +1077,9 @@ export class DownloadManager { ` (${validCount})`; const list = document.getElementById('batchPreviewList'); - list.innerHTML = this.batchModels.map((item, index) => { + const hasHfItems = this.batchModels.some(m => m.source === 'huggingface' && !m.error); + + let itemsHtml = this.batchModels.map((item, index) => { if (item.error) { return `
@@ -1137,19 +1102,16 @@ export class DownloadManager { // HF batch item rendering with checkbox if (item.source === 'huggingface') { const hfSize = item.fileSizeBytes - ? (item.fileSizeBytes / (1024 * 1024)).toFixed(1) + ? formatFileSize(item.fileSizeBytes) : '?'; return `
-
- -
${item.displayName || item.filename || `HF #${index}`} HF
- ${hfSize} MB + ${hfSize} ${item.repo || ''}
@@ -1189,6 +1151,21 @@ export class DownloadManager { `; }).join(''); + // Prepend select-all toolbar if there are HF items with checkboxes + if (hasHfItems) { + const allChecked = this.batchModels + .filter(m => m.source === 'huggingface' && !m.error) + .every(m => m.checked !== false); + itemsHtml = ` +
+ + +
+ ` + itemsHtml; + } + + list.innerHTML = itemsHtml; + list.onclick = (e) => { const removeBtn = e.target.closest('.batch-preview-remove'); if (removeBtn) { @@ -1212,16 +1189,51 @@ export class DownloadManager { if (this.batchModels[idx]) { this.batchModels[idx].checked = e.target.checked; } - // Update valid count in title + // Update valid count in title and Next button const checkedCount = this.batchModels.filter( m => !m.error && m.checked !== false ).length; document.getElementById('downloadModalTitle').textContent = translate('modals.download.titleWithType', { type: this.apiClient.apiConfig.config.displayName }) + ` (${checkedCount})`; + const nextBtn = document.getElementById('nextFromBatchBtn'); + nextBtn.disabled = checkedCount === 0; + nextBtn.classList.toggle('disabled', checkedCount === 0); + // Update select-all checkbox state + const selectAll = document.getElementById('batchSelectAll'); + if (selectAll) { + const hfItems = this.batchModels.filter(m => m.source === 'huggingface' && !m.error); + selectAll.checked = hfItems.length > 0 && hfItems.every(m => m.checked !== false); + } }); }); + // Select-all handler + const selectAll = document.getElementById('batchSelectAll'); + if (selectAll) { + selectAll.addEventListener('change', (e) => { + const checked = e.target.checked; + const hfCheckboxes = list.querySelectorAll('.batch-preview-checkbox'); + hfCheckboxes.forEach(cb => { + cb.checked = checked; + const idx = parseInt(cb.dataset.index); + if (this.batchModels[idx]) { + this.batchModels[idx].checked = checked; + } + }); + // Update valid count in title and Next button + const checkedCount = this.batchModels.filter( + m => !m.error && m.checked !== false + ).length; + document.getElementById('downloadModalTitle').textContent = + translate('modals.download.titleWithType', { type: this.apiClient.apiConfig.config.displayName }) + + ` (${checkedCount})`; + const nextBtn = document.getElementById('nextFromBatchBtn'); + nextBtn.disabled = checkedCount === 0; + nextBtn.classList.toggle('disabled', checkedCount === 0); + }); + } + const nextBtn = document.getElementById('nextFromBatchBtn'); nextBtn.disabled = validCount === 0; nextBtn.classList.toggle('disabled', validCount === 0); @@ -1360,7 +1372,7 @@ export class DownloadManager { if (data.status === 'progress' && data.download_id?.startsWith(batchDownloadId)) { const current = downloadItems[completedDownloads + failedDownloads]; - const name = current?.selectedVersion?.name || `#${completedDownloads + failedDownloads + 1}`; + const name = current?.selectedVersion?.name || current?.displayName || current?.filename || `#${completedDownloads + failedDownloads + 1}`; const metrics = { bytesDownloaded: data.bytes_downloaded, totalBytes: data.total_bytes, diff --git a/templates/components/modals/download_modal.html b/templates/components/modals/download_modal.html index 100c2e4b..b09acdb2 100644 --- a/templates/components/modals/download_modal.html +++ b/templates/components/modals/download_modal.html @@ -22,24 +22,6 @@
- - -