feat: add update available indicator to model cards

- Add CSS custom properties for update badge styling in both light and dark themes
- Create new card header info layout with flexbox for better content organization
- Implement model-update-badge component with glow effects and proper spacing
- Add has-update class to cards when updates are available with visual border indicators
- Update ModelCard.js to conditionally render update badges based on model data
- Include internationalization support for update badge labels and tooltips

The changes provide users with clear visual indicators when model updates are available, improving the user experience by making update status immediately visible without requiring manual checks.
This commit is contained in:
Will Miao
2025-10-25 16:41:35 +08:00
parent 2abb5bf122
commit 5fe0660c64
13 changed files with 99 additions and 4 deletions

View File

@@ -127,6 +127,10 @@
"checkError": "Fehler beim Überprüfen der Beispielbilder",
"missingHash": "Fehlende Modell-Hash-Informationen.",
"noRemoteImagesAvailable": "Keine Remote-Beispielbilder für dieses Modell auf Civitai verfügbar"
},
"badges": {
"update": "Update",
"updateAvailable": "Update verfügbar"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "Error checking for example images",
"missingHash": "Missing model hash information.",
"noRemoteImagesAvailable": "No remote example images available for this model on Civitai"
},
"badges": {
"update": "Update",
"updateAvailable": "Update available"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "Error al verificar imágenes de ejemplo",
"missingHash": "Falta información del hash del modelo.",
"noRemoteImagesAvailable": "No hay imágenes de ejemplo remotas disponibles para este modelo en Civitai"
},
"badges": {
"update": "Actualización",
"updateAvailable": "Actualización disponible"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "Erreur lors de la vérification des images d'exemple",
"missingHash": "Informations de hachage du modèle manquantes.",
"noRemoteImagesAvailable": "Aucune image d'exemple distante disponible pour ce modèle sur Civitai"
},
"badges": {
"update": "Mise à jour",
"updateAvailable": "Mise à jour disponible"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "שגיאה בבדיקת תמונות דוגמה",
"missingHash": "חסר מידע hash של המודל.",
"noRemoteImagesAvailable": "אין תמונות דוגמה מרוחקות זמינות למודל זה ב-Civitai"
},
"badges": {
"update": "עדכון",
"updateAvailable": "עדכון זמין"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "例画像の確認中にエラーが発生しました",
"missingHash": "モデルハッシュ情報がありません。",
"noRemoteImagesAvailable": "このモデルのCivitaiでのリモート例画像は利用できません"
},
"badges": {
"update": "アップデート",
"updateAvailable": "アップデートがあります"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "예시 이미지 확인 중 오류",
"missingHash": "모델 해시 정보가 없습니다.",
"noRemoteImagesAvailable": "Civitai에서 이 모델의 원격 예시 이미지를 사용할 수 없습니다"
},
"badges": {
"update": "업데이트",
"updateAvailable": "업데이트 가능"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "Ошибка проверки примеров изображений",
"missingHash": "Отсутствует хеш модели.",
"noRemoteImagesAvailable": "Нет удаленных примеров изображений для этой модели на Civitai"
},
"badges": {
"update": "Обновление",
"updateAvailable": "Доступно обновление"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "检查示例图片时出错",
"missingHash": "缺少模型哈希信息。",
"noRemoteImagesAvailable": "此模型在 Civitai 上没有远程示例图片"
},
"badges": {
"update": "更新",
"updateAvailable": "有可用更新"
}
},
"globalContextMenu": {

View File

@@ -127,6 +127,10 @@
"checkError": "檢查範例圖片時發生錯誤",
"missingHash": "缺少模型雜湊資訊。",
"noRemoteImagesAvailable": "此模型在 Civitai 上無遠端範例圖片"
},
"badges": {
"update": "更新",
"updateAvailable": "有可用更新"
}
},
"globalContextMenu": {

View File

@@ -53,6 +53,9 @@ html, body {
--lora-error: oklch(75% 0.32 29);
--lora-warning: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
--lora-success: oklch(var(--lora-success-l) var(--lora-success-c) var(--lora-success-h));
--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);
/* Spacing Scale */
--space-1: calc(8px * 1);
@@ -100,6 +103,9 @@ html[data-theme="light"] {
--lora-border: oklch(90% 0.02 256 / 0.15);
--lora-text: oklch(98% 0.02 256);
--lora-warning: oklch(75% 0.25 80); /* Modified to be used with oklch() */
--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);
}
body {

View File

@@ -296,6 +296,18 @@
min-height: 20px;
}
.card-header-info {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.card-header-info .base-model-label {
flex-shrink: 1;
}
.card-actions i {
margin-left: var(--space-1);
cursor: pointer;
@@ -422,6 +434,7 @@
border-radius: var(--border-radius-xs);
backdrop-filter: blur(2px);
font-size: 0.85em;
line-height: 1.2;
}
/* Style for version name */
@@ -575,4 +588,26 @@
15% { opacity: 1; transform: translateY(0); }
85% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(0); }
}
}
.model-card.has-update {
border-color: color-mix(in oklab, var(--badge-update-bg) 60%, transparent);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--badge-update-bg) 45%, transparent);
}
.model-update-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: var(--border-radius-xs);
background: var(--badge-update-bg);
color: var(--badge-update-text);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
box-shadow: 0 4px 12px var(--badge-update-glow);
border: 1px solid color-mix(in oklab, var(--badge-update-bg) 55%, transparent);
white-space: nowrap;
}

View File

@@ -432,6 +432,8 @@ export function createModelCard(model, modelType) {
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';
// LoRA specific data
if (modelType === MODEL_TYPES.LORA) {
@@ -507,6 +509,9 @@ export function createModelCard(model, modelType) {
// Get favorite status from model data
const isFavorite = model.favorite === true;
if (hasUpdateAvailable) {
card.classList.add('has-update');
}
// Generate action icons based on model type with i18n support
const favoriteTitle = isFavorite ?
@@ -531,6 +536,8 @@ export function createModelCard(model, modelType) {
copyTitle = translate('modelCard.actions.copyLoRASyntax', {}, 'Copy value');
}
const updateBadgeLabel = translate('modelCard.badges.update', {}, 'Update');
const updateBadgeTooltip = translate('modelCard.badges.updateAvailable', {}, 'Update available');
const actionIcons = `
<i class="${isFavorite ? 'fas fa-star favorite-active' : 'far fa-star'}"
title="${favoriteTitle}">
@@ -568,9 +575,16 @@ export function createModelCard(model, modelType) {
`<button class="toggle-blur-btn" title="${toggleBlurTitle}">
<i class="fas fa-eye"></i>
</button>` : ''}
<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${model.base_model}">
${model.base_model}
</span>
<div class="card-header-info">
<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${model.base_model}">
${model.base_model}
</span>
${hasUpdateAvailable ? `
<span class="model-update-badge" title="${updateBadgeTooltip}">
${updateBadgeLabel}
</span>
` : ''}
</div>
<div class="card-actions">
${actionIcons}
</div>