From 7e5e3b1ec73b208002a2cdc1a395d8f1030fb9e2 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 2 Jun 2026 15:41:42 +0800 Subject: [PATCH] feat(download): support multi-precision file selection for CivitAI model downloads (#956) --- locales/de.json | 5 + locales/en.json | 5 + locales/es.json | 5 + locales/fr.json | 5 + locales/he.json | 5 + locales/ja.json | 5 + locales/ko.json | 5 + locales/ru.json | 5 + locales/zh-CN.json | 5 + locales/zh-TW.json | 5 + .../css/components/modal/download-modal.css | 166 ++++++++++++++++++ static/js/api/baseModelApi.js | 5 +- static/js/managers/DownloadManager.js | 119 ++++++++++++- .../components/modals/download_modal.html | 15 ++ 14 files changed, 350 insertions(+), 5 deletions(-) diff --git a/locales/de.json b/locales/de.json index d73a5df8..6bf5fb63 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "Zuvor heruntergeladen, aber derzeit nicht in Ihrer Bibliothek.", "alreadyInLibrary": "Bereits in Bibliothek", "autoOrganizedPath": "[Automatisch organisiert durch Pfadvorlage]", + "fileSelection": { + "title": "Dateiformat auswählen", + "files": "Dateien", + "select": "Datei auswählen" + }, "errors": { "invalidUrl": "Ungültiges Civitai URL-Format", "noVersions": "Keine Versionen für dieses Modell verfügbar" diff --git a/locales/en.json b/locales/en.json index b9bc05ba..65f09b35 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "Previously downloaded, but it is not currently in your library.", "alreadyInLibrary": "Already in Library", "autoOrganizedPath": "[Auto-organized by path template]", + "fileSelection": { + "title": "Select File Format", + "files": "files", + "select": "Select File" + }, "errors": { "invalidUrl": "Invalid Civitai URL format", "noVersions": "No versions available for this model" diff --git a/locales/es.json b/locales/es.json index c5b7cde0..ae7871e9 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "Descargado anteriormente, pero actualmente no está en tu biblioteca.", "alreadyInLibrary": "Ya en la biblioteca", "autoOrganizedPath": "[Auto-organizado por plantilla de ruta]", + "fileSelection": { + "title": "Seleccionar formato de archivo", + "files": "archivos", + "select": "Seleccionar archivo" + }, "errors": { "invalidUrl": "Formato de URL de Civitai inválido", "noVersions": "No hay versiones disponibles para este modelo" diff --git a/locales/fr.json b/locales/fr.json index 60c129ef..de7dbab8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "Déjà téléchargé, mais il n'est actuellement pas dans votre bibliothèque.", "alreadyInLibrary": "Déjà dans la bibliothèque", "autoOrganizedPath": "[Auto-organisé par modèle de chemin]", + "fileSelection": { + "title": "Choisir le format de fichier", + "files": "fichiers", + "select": "Choisir le fichier" + }, "errors": { "invalidUrl": "Format d'URL Civitai invalide", "noVersions": "Aucune version disponible pour ce modèle" diff --git a/locales/he.json b/locales/he.json index f9088b8f..f88abfdc 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "הורד בעבר, אך הוא אינו נמצא כרגע בספרייה שלך.", "alreadyInLibrary": "כבר בספרייה", "autoOrganizedPath": "[מאורגן אוטומטית לפי תבנית נתיב]", + "fileSelection": { + "title": "בחר פורמט קובץ", + "files": "קבצים", + "select": "בחר קובץ" + }, "errors": { "invalidUrl": "פורמט URL של Civitai לא חוקי", "noVersions": "אין גרסאות זמינות למודל זה" diff --git a/locales/ja.json b/locales/ja.json index 677fe9db..72ab5a40 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "以前にダウンロード済みですが、現在はライブラリにありません。", "alreadyInLibrary": "既にライブラリ内", "autoOrganizedPath": "[パステンプレートによる自動整理]", + "fileSelection": { + "title": "ファイル形式を選択", + "files": "ファイル", + "select": "ファイルを選択" + }, "errors": { "invalidUrl": "無効なCivitai URL形式", "noVersions": "このモデルの利用可能なバージョンがありません" diff --git a/locales/ko.json b/locales/ko.json index 502b668a..f265f99f 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "이전에 다운로드했지만 현재 라이브러리에 없습니다.", "alreadyInLibrary": "이미 라이브러리에 있음", "autoOrganizedPath": "[경로 템플릿으로 자동 정리됨]", + "fileSelection": { + "title": "파일 형식 선택", + "files": "개 파일", + "select": "파일 선택" + }, "errors": { "invalidUrl": "잘못된 Civitai URL 형식", "noVersions": "이 모델에 사용 가능한 버전이 없습니다" diff --git a/locales/ru.json b/locales/ru.json index f8b346d0..d666060f 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "Ранее загружено, но сейчас этого нет в вашей библиотеке.", "alreadyInLibrary": "Уже в библиотеке", "autoOrganizedPath": "[Автоматически организовано по шаблону пути]", + "fileSelection": { + "title": "Выбрать формат файла", + "files": "файлов", + "select": "Выбрать файл" + }, "errors": { "invalidUrl": "Неверный формат URL Civitai", "noVersions": "Нет доступных версий для этой модели" diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 36e6f2d1..46f67846 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "之前已下载,但当前不在你的库中。", "alreadyInLibrary": "已存在于库中", "autoOrganizedPath": "【已按路径模板自动整理】", + "fileSelection": { + "title": "选择文件格式", + "files": "个文件", + "select": "选择文件" + }, "errors": { "invalidUrl": "无效的 Civitai URL 格式", "noVersions": "此模型没有可用版本" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index b2cd78ef..4ff95aba 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1031,6 +1031,11 @@ "downloadedTooltip": "先前已下載,但目前不在你的庫中。", "alreadyInLibrary": "已在庫存", "autoOrganizedPath": "[依路徑範本自動整理]", + "fileSelection": { + "title": "選擇檔案格式", + "files": "個檔案", + "select": "選擇檔案" + }, "errors": { "invalidUrl": "Civitai 網址格式無效", "noVersions": "此模型無可用版本" diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index d146b87b..33a1c2b4 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -502,4 +502,170 @@ opacity: 0.5; pointer-events: none; user-select: none; +} + +/* File Count Badge on Version Items */ +.file-select-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + background: oklch(var(--lora-accent) / 0.18); + color: var(--lora-accent); + font-size: inherit; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid oklch(var(--lora-accent) / 0.35); + user-select: none; + box-shadow: 0 1px 2px oklch(var(--lora-accent) / 0.1); +} + +.file-select-badge:hover { + background: oklch(var(--lora-accent) / 0.3); + border-color: var(--lora-accent); + transform: scale(1.05); + box-shadow: 0 2px 6px oklch(var(--lora-accent) / 0.2); +} + +.file-select-badge:active { + transform: scale(0.98); +} + +.file-select-badge i { + font-size: 0.9em; +} + +.file-select-badge .badge-arrow { + margin-left: 2px; + font-size: 0.65em; + opacity: 0.7; +} + +/* File Selection Step */ +.file-selection-header { + margin-bottom: var(--space-3); +} + +.file-selection-header h3 { + margin: 0 0 4px 0; + font-size: 1.1em; + color: var(--text-color); +} + +.file-selection-version-name { + font-size: 0.9em; + color: var(--text-color); + opacity: 0.7; +} + +.file-selection-list { + max-height: 360px; + overflow-y: auto; + margin: var(--space-2) 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.file-option { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: all 0.2s ease; + background: var(--bg-color); +} + +.file-option:hover { + border-color: var(--lora-accent); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); +} + +.file-option.selected { + border: 2px solid var(--lora-accent); + background: oklch(var(--lora-accent) / 0.05); +} + +.file-option-radio { + flex-shrink: 0; +} + +.file-option-radio input[type="radio"] { + width: 16px; + height: 16px; + accent-color: var(--lora-accent); + cursor: pointer; +} + +.file-option-info { + flex: 1; + min-width: 0; +} + +.file-option-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 4px; +} + +.file-tag { + display: inline-block; + padding: 2px 7px; + border-radius: 4px; + font-size: 0.8em; + font-weight: 500; + line-height: 1.4; +} + +.file-tag.format { + background: oklch(var(--lora-accent) / 0.1); + color: var(--lora-accent); +} + +.file-tag.fp { + background: oklch(0.6 0.15 250 / 0.1); + color: oklch(0.55 0.15 250); +} + +.file-tag.size { + background: oklch(0.55 0.1 160 / 0.1); + color: oklch(0.5 0.12 160); +} + +.file-option-name { + font-size: 0.8em; + color: var(--text-color); + opacity: 0.6; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; +} + +.file-option-size { + font-size: 0.9em; + color: var(--text-color); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} + +/* Dark theme adjustments */ +[data-theme="dark"] .file-option { + background: var(--lora-surface); +} + +[data-theme="dark"] .file-tag.fp { + background: oklch(0.55 0.12 250 / 0.15); + color: oklch(0.7 0.12 250); +} + +[data-theme="dark"] .file-tag.size { + background: oklch(0.5 0.08 160 / 0.15); + color: oklch(0.65 0.08 160); } \ No newline at end of file diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 750c2f21..c80d0fd3 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -909,7 +909,7 @@ export class BaseModelApiClient { } } - async downloadModel(modelId, versionId, modelRoot, relativePath, useDefaultPaths = false, downloadId, source = null) { + async downloadModel(modelId, versionId, modelRoot, relativePath, useDefaultPaths = false, downloadId, source = null, fileParams = null) { try { const response = await fetch(DOWNLOAD_ENDPOINTS.download, { method: 'POST', @@ -921,7 +921,8 @@ export class BaseModelApiClient { relative_path: relativePath, use_default_paths: useDefaultPaths, download_id: downloadId, - ...(source ? { source } : {}) + ...(source ? { source } : {}), + ...(fileParams ? { file_params: fileParams } : {}) }) }); diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 52ed7085..48dcbe3c 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -33,6 +33,8 @@ export class DownloadManager { this.handleStartDownload = this.startDownload.bind(this); this.handleBackToUrl = this.backToUrl.bind(this); this.handleBackToVersions = this.backToVersions.bind(this); + this.handleBackToVersionFromFiles = this.backToVersionFromFiles.bind(this); + this.handleConfirmFileSelection = this.confirmFileSelection.bind(this); this.handleCloseModal = this.closeModal.bind(this); this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this); } @@ -80,6 +82,10 @@ export class DownloadManager { document.getElementById('backToVersionsBtn').addEventListener('click', this.handleBackToVersions); document.getElementById('closeDownloadModal').addEventListener('click', this.handleCloseModal); + // File selection step buttons + document.getElementById('backToVersionFromFilesBtn').addEventListener('click', this.handleBackToVersionFromFiles); + document.getElementById('confirmFileSelection').addEventListener('click', this.handleConfirmFileSelection); + // Default path toggle handler document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); } @@ -129,6 +135,7 @@ export class DownloadManager { this.modelId = null; this.modelVersionId = null; this.source = null; + this.selectedFile = null; this.selectedFolder = ''; @@ -247,9 +254,12 @@ export class DownloadManager { const firstImage = version.images?.find(img => !img.url.endsWith('.mp4')); const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png'; + // Count model-type files per version + const modelFiles = (version.files || []).filter(f => f.type === 'Model'); + const primaryFile = modelFiles.find(f => f.primary) || modelFiles[0] || {}; const fileSize = version.modelSizeKB ? (version.modelSizeKB / 1024).toFixed(2) : - (version.files[0]?.sizeKB / 1024).toFixed(2); + ((primaryFile.sizeKB || 0) / 1024).toFixed(2); const existsLocally = version.existsLocally; const hasBeenDownloaded = version.hasBeenDownloaded && !existsLocally; @@ -282,6 +292,12 @@ export class DownloadManager { `; } + const fileBadge = modelFiles.length > 1 && !existsLocally + ? ` + ${modelFiles.length} ${translate('modals.download.fileSelection.files')} + ` + : ''; + return `
${new Date(version.createdAt).toLocaleDateString()} ${fileSize} MB + ${fileBadge}
`; }).join(''); - // Add click handlers for version selection + // Add click handlers for version selection and file badge versionList.addEventListener('click', (event) => { + const badge = event.target.closest('.file-select-badge'); + if (badge) { + event.stopPropagation(); + const versionId = badge.dataset.versionId; + this.selectVersion(versionId); + this.showFileSelectionStep(versionId); + return; + } const versionItem = event.target.closest('.version-item'); if (versionItem) { this.selectVersion(versionItem.dataset.versionId); @@ -352,6 +377,80 @@ export class DownloadManager { } } + showFileSelectionStep(versionId) { + const version = this.versions.find(v => v.id.toString() === versionId.toString()); + if (!version) return; + + this.currentVersion = version; + const modelFiles = (version.files || []).filter(f => f.type === 'Model'); + + document.getElementById('versionStep').style.display = 'none'; + document.getElementById('fileSelectionStep').style.display = 'block'; + + const nameEl = document.getElementById('fileSelectionVersionName'); + if (nameEl) { + nameEl.textContent = `${version.name} · ${version.baseModel || ''}`; + } + + const container = document.getElementById('fileSelectionList'); + container.innerHTML = modelFiles.map(file => { + const meta = file.metadata || {}; + const sizeGB = file.sizeKB ? (file.sizeKB / (1024 * 1024)).toFixed(2) : '--'; + const isSelected = this.selectedFile?.id === file.id; + + const tags = []; + if (meta.size) tags.push(`${meta.size}`); + if (meta.format) tags.push(`${meta.format}`); + if (meta.fp) tags.push(`${meta.fp}`); + + const fileName = file.name || ''; + + return ` +
+
+ +
+
+
+ ${tags.join(' ')} +
+
${fileName}
+
+
${sizeGB} GB
+
+ `; + }).join(''); + + container.querySelectorAll('.file-option').forEach(el => { + el.addEventListener('click', () => { + container.querySelectorAll('.file-option').forEach(o => o.classList.remove('selected')); + el.classList.add('selected'); + const radio = el.querySelector('input[type="radio"]'); + if (radio) radio.checked = true; + }); + }); + } + + confirmFileSelection() { + const selectedRadio = document.querySelector('#fileSelectionList input[type="radio"]:checked'); + if (!selectedRadio) return; + + const version = this.currentVersion; + if (!version) return; + + const modelFiles = (version.files || []).filter(f => f.type === 'Model'); + this.selectedFile = modelFiles.find(f => f.id.toString() === selectedRadio.value); + + document.getElementById('fileSelectionStep').style.display = 'none'; + document.getElementById('locationStep').style.display = 'block'; + this.proceedToLocationContent(); + } + + backToVersionFromFiles() { + document.getElementById('fileSelectionStep').style.display = 'none'; + document.getElementById('versionStep').style.display = 'block'; + } + async proceedToLocation() { if (!this.currentVersion) { showToast('toast.loras.pleaseSelectVersion', {}, 'error'); @@ -366,6 +465,10 @@ export class DownloadManager { document.getElementById('versionStep').style.display = 'none'; document.getElementById('locationStep').style.display = 'block'; + await this.proceedToLocationContent(); + } + + async proceedToLocationContent() { try { // Fetch model roots @@ -450,6 +553,7 @@ export class DownloadManager { targetFolder = '', useDefaultPaths = false, source = null, + fileParams = null, closeModal = false, }) { const config = this.apiClient?.apiConfig?.config; @@ -513,7 +617,8 @@ export class DownloadManager { targetFolder, useDefaultPaths, downloadId, - source + source, + fileParams ); if (response?.skipped) { @@ -632,6 +737,13 @@ export class DownloadManager { } else { targetFolder = this.folderTreeManager.getSelectedPath(); } + const fileParams = this.selectedFile ? { + type: 'Model', + format: this.selectedFile.metadata?.format || 'SafeTensor', + size: this.selectedFile.metadata?.size || 'full', + fp: this.selectedFile.metadata?.fp, + } : null; + return this.executeDownloadWithProgress({ modelId: this.modelId, versionId: this.currentVersion.id, @@ -640,6 +752,7 @@ export class DownloadManager { targetFolder, useDefaultPaths, source: this.source, + fileParams, closeModal: true, }); } diff --git a/templates/components/modals/download_modal.html b/templates/components/modals/download_modal.html index 32955384..399ac3d6 100644 --- a/templates/components/modals/download_modal.html +++ b/templates/components/modals/download_modal.html @@ -29,6 +29,21 @@ + + +