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
This commit is contained in:
Will Miao
2026-06-30 23:28:35 +08:00
parent e8913f4481
commit 7cf785b72f
13 changed files with 130 additions and 172 deletions

View File

@@ -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;
}
}
/* 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);
}

View File

@@ -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 `
<div class="repo-file-item" data-index="${i}">
<input type="checkbox" class="repo-file-checkbox" data-index="${i}" />
<span class="repo-file-icon"><i class="fas fa-file"></i></span>
<span class="repo-file-name">${f.filename}</span>
<span class="repo-file-meta">
<span class="repo-file-size">${sizeMb} MB</span>
</span>
</div>
`;
}).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 `
<div class="batch-preview-item batch-preview-error" data-index="${index}">
@@ -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 `
<div class="batch-preview-item" data-index="${index}">
<input type="checkbox" class="batch-preview-checkbox"
data-index="${index}" ${item.checked !== false ? 'checked' : ''} />
<div class="batch-preview-icon" style="color: var(--lora-accent);">
<i class="fas fa-cloud"></i>
</div>
<div class="batch-preview-info">
<div class="batch-preview-name">${item.displayName || item.filename || `HF #${index}`} <span class="hf-badge">HF</span></div>
<div class="batch-preview-meta">
<span>${hfSize} MB</span>
<span>${hfSize}</span>
<span>${item.repo || ''}</span>
</div>
</div>
@@ -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 = `
<div class="batch-preview-select-all">
<input type="checkbox" id="batchSelectAll" ${allChecked ? 'checked' : ''} />
<label for="batchSelectAll">${translate('modals.download.selectAll', {}, 'Select All')}</label>
</div>
` + 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,