feat: enhance skip metadata refresh with smart UI and subtle badges, #790

This commit is contained in:
Will Miao
2026-02-09 09:15:29 +08:00
parent 2b74b2373d
commit 765c1c42a9
23 changed files with 283 additions and 18 deletions

View File

@@ -60,6 +60,9 @@ body {
--badge-update-bg: oklch(72% 0.2 220);
--badge-update-text: oklch(28% 0.03 220);
--badge-update-glow: oklch(72% 0.2 220 / 0.28);
--badge-skip-refresh-bg: oklch(82% 0.12 45);
--badge-skip-refresh-text: oklch(35% 0.02 45);
--badge-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
/* Spacing Scale */
--space-1: calc(8px * 1);
@@ -114,6 +117,9 @@ html[data-theme="light"] {
--badge-update-bg: oklch(62% 0.18 220);
--badge-update-text: oklch(98% 0.02 240);
--badge-update-glow: oklch(62% 0.18 220 / 0.4);
--badge-skip-refresh-bg: oklch(82% 0.12 45);
--badge-skip-refresh-text: oklch(98% 0.02 45);
--badge-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
}
body {

View File

@@ -658,3 +658,25 @@
margin-left: 1px;
line-height: 1;
}
.model-skip-refresh-badge {
width: 16px;
height: 16px;
padding: 0;
border-radius: 3px;
background: var(--badge-skip-refresh-bg);
color: var(--badge-skip-refresh-text);
font-size: 0.65rem;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 1px 3px var(--badge-skip-refresh-glow);
border: 1px solid color-mix(in oklab, var(--badge-skip-refresh-bg) 70%, transparent);
opacity: 0.85;
}
.model-skip-refresh-badge i {
margin-left: 0;
line-height: 1;
}

View File

@@ -1,7 +1,7 @@
import { BaseContextMenu } from './BaseContextMenu.js';
import { state } from '../../state/index.js';
import { bulkManager } from '../../managers/BulkManager.js';
import { updateElementText } from '../../utils/i18nHelpers.js';
import { updateElementText, translate } from '../../utils/i18nHelpers.js';
export class BulkContextMenu extends BaseContextMenu {
constructor() {
@@ -71,6 +71,40 @@ export class BulkContextMenu extends BaseContextMenu {
if (setContentRatingItem) {
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none';
}
const skipMetadataRefreshItem = this.menu.querySelector('[data-action="skip-metadata-refresh"]');
const resumeMetadataRefreshItem = this.menu.querySelector('[data-action="resume-metadata-refresh"]');
if (skipMetadataRefreshItem && resumeMetadataRefreshItem) {
const skipCount = this.countSkipStatus(true);
const resumeCount = this.countSkipStatus(false);
const totalCount = skipCount + resumeCount;
if (skipCount === totalCount) {
skipMetadataRefreshItem.style.display = 'none';
resumeMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.resumeMetadataRefresh'
);
} else if (resumeCount === totalCount) {
skipMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.style.display = 'none';
skipMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.skipMetadataRefresh'
);
} else {
skipMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.style.display = 'flex';
skipMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.skipMetadataRefreshCount',
{ count: resumeCount }
);
resumeMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.resumeMetadataRefreshCount',
{ count: skipCount }
);
}
}
}
updateSelectedCountHeader() {
@@ -80,6 +114,20 @@ export class BulkContextMenu extends BaseContextMenu {
}
}
countSkipStatus(skipState) {
let count = 0;
for (const filePath of state.selectedModels) {
const card = document.querySelector(`.model-card[data-filepath="${filePath}"]`);
if (card) {
const isSkipped = card.dataset.skip_metadata_refresh === 'true';
if (isSkipped === skipState) {
count++;
}
}
}
return count;
}
showMenu(x, y, card) {
this.updateMenuItemsForModelType();
this.updateSelectedCountHeader();
@@ -118,6 +166,12 @@ export class BulkContextMenu extends BaseContextMenu {
case 'auto-organize':
bulkManager.autoOrganizeSelectedModels();
break;
case 'skip-metadata-refresh':
bulkManager.setSkipMetadataRefresh(true);
break;
case 'resume-metadata-refresh':
bulkManager.setSkipMetadataRefresh(false);
break;
case 'delete-all':
bulkManager.showBulkDeleteModal();
break;

View File

@@ -433,9 +433,10 @@ export function createModelCard(model, modelType) {
card.dataset.usage_count = String(model.usage_count);
card.dataset.notes = model.notes || '';
card.dataset.base_model = model.base_model || 'Unknown';
card.dataset.favorite = model.favorite ? 'true' : 'false';
const hasUpdateAvailable = Boolean(model.update_available);
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
card.dataset.favorite = model.favorite ? 'true' : 'false';
const hasUpdateAvailable = Boolean(model.update_available);
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
card.dataset.skip_metadata_refresh = model.skip_metadata_refresh ? 'true' : 'false';
// To only show usage_count when sorting by usage.
const pageState = getCurrentPageState();
@@ -482,6 +483,10 @@ export function createModelCard(model, modelType) {
card.classList.add('nsfw-content');
}
if (model.skip_metadata_refresh) {
card.classList.add('skip-refresh');
}
// Apply selection state if in bulk mode and this card is in the selected set (LoRA only)
if (modelType === MODEL_TYPES.LORA && state.bulkMode && state.selectedLoras.has(model.file_path)) {
card.classList.add('selected');
@@ -608,6 +613,11 @@ export function createModelCard(model, modelType) {
<i class="fas fa-arrow-up"></i>
</span>
` : ''}
${model.skip_metadata_refresh ? `
<span class="model-skip-refresh-badge" title="${translate('modelCard.badges.skipRefresh', {}, 'Metadata refresh skipped')}">
<i class="fas fa-ban"></i>
</span>
` : ''}
</div>
<div class="card-actions">
${actionIcons}

View File

@@ -40,7 +40,8 @@ export class BulkManager {
moveAll: true,
autoOrganize: true,
deleteAll: true,
setContentRating: true
setContentRating: true,
skipMetadataRefresh: true
},
[MODEL_TYPES.EMBEDDING]: {
addTags: true,
@@ -51,7 +52,8 @@ export class BulkManager {
moveAll: true,
autoOrganize: true,
deleteAll: true,
setContentRating: false
setContentRating: false,
skipMetadataRefresh: true
},
[MODEL_TYPES.CHECKPOINT]: {
addTags: true,
@@ -62,7 +64,8 @@ export class BulkManager {
moveAll: false,
autoOrganize: true,
deleteAll: true,
setContentRating: true
setContentRating: true,
skipMetadataRefresh: true
},
recipes: {
addTags: false,
@@ -73,7 +76,8 @@ export class BulkManager {
moveAll: true,
autoOrganize: false,
deleteAll: true,
setContentRating: false
setContentRating: false,
skipMetadataRefresh: false
}
};
@@ -1195,6 +1199,59 @@ export class BulkManager {
return successCount > 0;
}
async setSkipMetadataRefresh(value) {
if (state.selectedModels.size === 0) {
showToast('toast.models.noModelsSelected', {}, 'warning');
return;
}
const totalCount = state.selectedModels.size;
state.loadingManager.showSimpleLoading(
translate('toast.models.skipMetadataRefreshUpdating', { count: totalCount })
);
let cancelled = false;
state.loadingManager.showCancelButton(() => {
cancelled = true;
});
let successCount = 0;
let failureCount = 0;
try {
const apiClient = getModelApiClient();
for (const filePath of state.selectedModels) {
if (cancelled) {
showToast('toast.api.operationCancelled', {}, 'info');
break;
}
try {
await apiClient.saveModelMetadata(filePath, { skip_metadata_refresh: value });
successCount++;
} catch (error) {
failureCount++;
console.error(`Failed to set skip_metadata_refresh for ${filePath}:`, error);
}
}
} finally {
state.loadingManager?.hide?.();
}
if (successCount === totalCount) {
const toastKey = value
? 'toast.models.skipMetadataRefreshSet'
: 'toast.models.skipMetadataRefreshCleared';
showToast(toastKey, { count: successCount }, 'success');
} else if (successCount > 0) {
showToast('toast.models.skipMetadataRefreshPartial', {
success: successCount,
failed: failureCount
}, 'warning');
} else {
showToast('toast.models.skipMetadataRefreshFailed', {}, 'error');
}
}
/**
* Initialize bulk base model interface
*/