From 765c1c42a95485c58da3bb02c5b8ad6d979a80d1 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 9 Feb 2026 09:15:29 +0800 Subject: [PATCH] feat: enhance skip metadata refresh with smart UI and subtle badges, #790 --- locales/de.json | 12 +++- locales/en.json | 12 +++- locales/es.json | 12 +++- locales/fr.json | 12 +++- locales/he.json | 12 +++- locales/ja.json | 12 +++- locales/ko.json | 12 +++- locales/ru.json | 12 +++- locales/zh-CN.json | 12 +++- locales/zh-TW.json | 12 +++- py/services/checkpoint_service.py | 1 + py/services/embedding_service.py | 1 + py/services/lora_service.py | 1 + py/services/model_scanner.py | 1 + py/services/persistent_model_cache.py | 4 ++ .../bulk_metadata_refresh_use_case.py | 1 + py/utils/models.py | 1 + static/css/base.css | 6 ++ static/css/components/card.css | 22 +++++++ .../components/ContextMenu/BulkContextMenu.js | 56 +++++++++++++++- static/js/components/shared/ModelCard.js | 16 ++++- static/js/managers/BulkManager.js | 65 +++++++++++++++++-- templates/components/context_menu.html | 6 ++ 23 files changed, 283 insertions(+), 18 deletions(-) diff --git a/locales/de.json b/locales/de.json index 6e2294a3..8ebd19fa 100644 --- a/locales/de.json +++ b/locales/de.json @@ -131,7 +131,8 @@ }, "badges": { "update": "Update", - "updateAvailable": "Update verfügbar" + "updateAvailable": "Update verfügbar", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "Verwendungsanzahl" @@ -527,8 +528,12 @@ "checkUpdates": "Auswahl auf Updates prüfen", "moveAll": "Alle in Ordner verschieben", "autoOrganize": "Automatisch organisieren", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "Alle Modelle löschen", "clear": "Auswahl löschen", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "Automatische Organisation wird initialisiert...", "starting": "Automatische Organisation für {type} wird gestartet...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "Basis-Modell erfolgreich für {count} Modell(e) aktualisiert", "bulkBaseModelUpdatePartial": "{success} Modelle aktualisiert, {failed} fehlgeschlagen", "bulkBaseModelUpdateFailed": "Aktualisierung des Basis-Modells für ausgewählte Modelle fehlgeschlagen", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "Inhaltsbewertung wird für {count} Modell(e) aktualisiert...", "bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt", "bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen", diff --git a/locales/en.json b/locales/en.json index d3e2f06d..7ffdf026 100644 --- a/locales/en.json +++ b/locales/en.json @@ -131,7 +131,8 @@ }, "badges": { "update": "Update", - "updateAvailable": "Update available" + "updateAvailable": "Update available", + "skipRefresh": "Metadata refresh skipped" }, "usage": { "timesUsed": "Times used" @@ -527,8 +528,12 @@ "checkUpdates": "Check Updates for Selected", "moveAll": "Move Selected to Folder", "autoOrganize": "Auto-Organize Selected", + "skipMetadataRefresh": "Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "Resume Metadata Refresh for Selected", "deleteAll": "Delete Selected Models", "clear": "Clear Selection", + "skipMetadataRefreshCount": "Skip ({count} models)", + "resumeMetadataRefreshCount": "Resume ({count} models)", "autoOrganizeProgress": { "initializing": "Initializing auto-organize...", "starting": "Starting auto-organize for {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "Successfully updated base model for {count} model(s)", "bulkBaseModelUpdatePartial": "Updated {success} model(s), failed {failed} model(s)", "bulkBaseModelUpdateFailed": "Failed to update base model for selected models", + "skipMetadataRefreshUpdating": "Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "Updating content rating for {count} model(s)...", "bulkContentRatingSet": "Set content rating to {level} for {count} model(s)", "bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed", diff --git a/locales/es.json b/locales/es.json index 0a9d436f..ba819d89 100644 --- a/locales/es.json +++ b/locales/es.json @@ -131,7 +131,8 @@ }, "badges": { "update": "Actualización", - "updateAvailable": "Actualización disponible" + "updateAvailable": "Actualización disponible", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "Veces usado" @@ -527,8 +528,12 @@ "checkUpdates": "Comprobar actualizaciones para la selección", "moveAll": "Mover todos a carpeta", "autoOrganize": "Auto-organizar seleccionados", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "Eliminar todos los modelos", "clear": "Limpiar selección", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "Inicializando auto-organización...", "starting": "Iniciando auto-organización para {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "Modelo base actualizado exitosamente para {count} modelo(s)", "bulkBaseModelUpdatePartial": "Actualizados {success} modelo(s), fallaron {failed} modelo(s)", "bulkBaseModelUpdateFailed": "Error al actualizar el modelo base para los modelos seleccionados", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "Actualizando la clasificación de contenido para {count} modelo(s)...", "bulkContentRatingSet": "Clasificación de contenido establecida en {level} para {count} modelo(s)", "bulkContentRatingPartial": "Clasificación de contenido establecida en {level} para {success} modelo(s), {failed} fallaron", diff --git a/locales/fr.json b/locales/fr.json index 397dd488..d12c11c8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -131,7 +131,8 @@ }, "badges": { "update": "Mise à jour", - "updateAvailable": "Mise à jour disponible" + "updateAvailable": "Mise à jour disponible", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "Nombre d'utilisations" @@ -527,8 +528,12 @@ "checkUpdates": "Vérifier les mises à jour pour la sélection", "moveAll": "Déplacer tout vers un dossier", "autoOrganize": "Auto-organiser la sélection", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "Supprimer tous les modèles", "clear": "Effacer la sélection", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "Initialisation de l'auto-organisation...", "starting": "Démarrage de l'auto-organisation pour {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "Modèle de base mis à jour avec succès pour {count} modèle(s)", "bulkBaseModelUpdatePartial": "{success} modèle(s) mis à jour, {failed} modèle(s) en échec", "bulkBaseModelUpdateFailed": "Échec de la mise à jour du modèle de base pour les modèles sélectionnés", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "Mise à jour de la classification du contenu pour {count} modèle(s)...", "bulkContentRatingSet": "Classification du contenu définie sur {level} pour {count} modèle(s)", "bulkContentRatingPartial": "Classification du contenu définie sur {level} pour {success} modèle(s), {failed} échec(s)", diff --git a/locales/he.json b/locales/he.json index 734b956d..bca27b8a 100644 --- a/locales/he.json +++ b/locales/he.json @@ -131,7 +131,8 @@ }, "badges": { "update": "עדכון", - "updateAvailable": "עדכון זמין" + "updateAvailable": "עדכון זמין", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "מספר שימושים" @@ -527,8 +528,12 @@ "checkUpdates": "בדוק עדכונים לבחירה", "moveAll": "העבר הכל לתיקייה", "autoOrganize": "ארגן אוטומטית נבחרים", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "מחק את כל המודלים", "clear": "נקה בחירה", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "מאתחל ארגון אוטומטי...", "starting": "מתחיל ארגון אוטומטי עבור {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "עודכן בהצלחה מודל הבסיס עבור {count} מודל(ים)", "bulkBaseModelUpdatePartial": "עודכנו {success} מודל(ים), נכשלו {failed} מודל(ים)", "bulkBaseModelUpdateFailed": "עדכון מודל הבסיס עבור המודלים שנבחרו נכשל", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "מעדכן דירוג תוכן עבור {count} מודלים...", "bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים", "bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו", diff --git a/locales/ja.json b/locales/ja.json index 7de32774..9dce6b2d 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -131,7 +131,8 @@ }, "badges": { "update": "アップデート", - "updateAvailable": "アップデートがあります" + "updateAvailable": "アップデートがあります", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "使用回数" @@ -527,8 +528,12 @@ "checkUpdates": "選択項目の更新を確認", "moveAll": "すべてをフォルダに移動", "autoOrganize": "自動整理を実行", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "すべてのモデルを削除", "clear": "選択をクリア", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "自動整理を初期化中...", "starting": "{type}の自動整理を開始中...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "{count} モデルのベースモデルが正常に更新されました", "bulkBaseModelUpdatePartial": "{success} モデルを更新、{failed} モデルは失敗しました", "bulkBaseModelUpdateFailed": "選択したモデルのベースモデルの更新に失敗しました", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "{count} 件のモデルのコンテンツレーティングを更新中...", "bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました", "bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました", diff --git a/locales/ko.json b/locales/ko.json index d7af5309..8784ca10 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -131,7 +131,8 @@ }, "badges": { "update": "업데이트", - "updateAvailable": "업데이트 가능" + "updateAvailable": "업데이트 가능", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "사용 횟수" @@ -527,8 +528,12 @@ "checkUpdates": "선택 항목 업데이트 확인", "moveAll": "모두 폴더로 이동", "autoOrganize": "자동 정리 선택", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "모든 모델 삭제", "clear": "선택 지우기", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "자동 정리 초기화 중...", "starting": "{type}에 대한 자동 정리 시작...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "{count}개의 모델에 베이스 모델이 성공적으로 업데이트되었습니다", "bulkBaseModelUpdatePartial": "{success}개의 모델이 업데이트되었고, {failed}개의 모델이 실패했습니다", "bulkBaseModelUpdateFailed": "선택한 모델의 베이스 모델 업데이트에 실패했습니다", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "{count}개 모델의 콘텐츠 등급을 업데이트하는 중...", "bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다", "bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다", diff --git a/locales/ru.json b/locales/ru.json index 4e03e2b4..0e753bac 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -131,7 +131,8 @@ }, "badges": { "update": "Обновление", - "updateAvailable": "Доступно обновление" + "updateAvailable": "Доступно обновление", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "Количество использований" @@ -527,8 +528,12 @@ "checkUpdates": "Проверить обновления для выбранных", "moveAll": "Переместить все в папку", "autoOrganize": "Автоматически организовать выбранные", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "Удалить все модели", "clear": "Очистить выбор", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "Инициализация автоматической организации...", "starting": "Запуск автоматической организации для {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "Базовая модель успешно обновлена для {count} моделей", "bulkBaseModelUpdatePartial": "Обновлено {success} моделей, не удалось обновить {failed} моделей", "bulkBaseModelUpdateFailed": "Не удалось обновить базовую модель для выбранных моделей", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "Обновление рейтинга контента для {count} модель(ей)...", "bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)", "bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 916040c3..6fc43e51 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -131,7 +131,8 @@ }, "badges": { "update": "更新", - "updateAvailable": "有可用更新" + "updateAvailable": "有可用更新", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "使用次数" @@ -527,8 +528,12 @@ "checkUpdates": "检查所选更新", "moveAll": "移动所选中到文件夹", "autoOrganize": "自动整理所选模型", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "删除选中模型", "clear": "清除选择", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "正在初始化自动整理...", "starting": "正在为 {type} 启动自动整理...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "成功为 {count} 个模型更新基础模型", "bulkBaseModelUpdatePartial": "更新了 {success} 个模型,{failed} 个失败", "bulkBaseModelUpdateFailed": "为选中模型更新基础模型失败", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "正在为 {count} 个模型更新内容评级...", "bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}", "bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 256c5a1f..9dbc89cf 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -131,7 +131,8 @@ }, "badges": { "update": "更新", - "updateAvailable": "有可用更新" + "updateAvailable": "有可用更新", + "skipRefresh": "[TODO: Translate] Metadata refresh skipped" }, "usage": { "timesUsed": "使用次數" @@ -527,8 +528,12 @@ "checkUpdates": "檢查所選更新", "moveAll": "全部移動到資料夾", "autoOrganize": "自動整理所選模型", + "skipMetadataRefresh": "[TODO: Translate] Skip Metadata Refresh for Selected", + "resumeMetadataRefresh": "[TODO: Translate] Resume Metadata Refresh for Selected", "deleteAll": "刪除全部模型", "clear": "清除選取", + "skipMetadataRefreshCount": "[TODO: Translate] Skip ({count} models)", + "resumeMetadataRefreshCount": "[TODO: Translate] Resume ({count} models)", "autoOrganizeProgress": { "initializing": "正在初始化自動整理...", "starting": "正在開始自動整理 {type}...", @@ -1379,6 +1384,11 @@ "bulkBaseModelUpdateSuccess": "已成功為 {count} 個模型更新基礎模型", "bulkBaseModelUpdatePartial": "已更新 {success} 個模型,{failed} 個模型失敗", "bulkBaseModelUpdateFailed": "更新所選模型的基礎模型失敗", + "skipMetadataRefreshUpdating": "[TODO: Translate] Updating metadata refresh flag for {count} model(s)...", + "skipMetadataRefreshSet": "[TODO: Translate] Metadata refresh skipped for {count} model(s)", + "skipMetadataRefreshCleared": "[TODO: Translate] Metadata refresh resumed for {count} model(s)", + "skipMetadataRefreshPartial": "[TODO: Translate] Updated {success} model(s), {failed} failed", + "skipMetadataRefreshFailed": "[TODO: Translate] Failed to update metadata refresh flag for selected models", "bulkContentRatingUpdating": "正在為 {count} 個模型更新內容分級...", "bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}", "bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗", diff --git a/py/services/checkpoint_service.py b/py/services/checkpoint_service.py index 5c496d09..64b0e1a9 100644 --- a/py/services/checkpoint_service.py +++ b/py/services/checkpoint_service.py @@ -43,6 +43,7 @@ class CheckpointService(BaseModelService): "sub_type": sub_type, "favorite": checkpoint_data.get("favorite", False), "update_available": bool(checkpoint_data.get("update_available", False)), + "skip_metadata_refresh": bool(checkpoint_data.get("skip_metadata_refresh", False)), "civitai": self.filter_civitai_data(checkpoint_data.get("civitai", {}), minimal=True) } diff --git a/py/services/embedding_service.py b/py/services/embedding_service.py index 252c8c65..b0959fd9 100644 --- a/py/services/embedding_service.py +++ b/py/services/embedding_service.py @@ -43,6 +43,7 @@ class EmbeddingService(BaseModelService): "sub_type": sub_type, "favorite": embedding_data.get("favorite", False), "update_available": bool(embedding_data.get("update_available", False)), + "skip_metadata_refresh": bool(embedding_data.get("skip_metadata_refresh", False)), "civitai": self.filter_civitai_data(embedding_data.get("civitai", {}), minimal=True) } diff --git a/py/services/lora_service.py b/py/services/lora_service.py index 424230d9..6b22209c 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -48,6 +48,7 @@ class LoraService(BaseModelService): "notes": lora_data.get("notes", ""), "favorite": lora_data.get("favorite", False), "update_available": bool(lora_data.get("update_available", False)), + "skip_metadata_refresh": bool(lora_data.get("skip_metadata_refresh", False)), "sub_type": sub_type, "civitai": self.filter_civitai_data( lora_data.get("civitai", {}), minimal=True diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index 3a710c86..6a99debd 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -248,6 +248,7 @@ class ModelScanner: 'tags': tags_list, 'civitai': civitai_slim, 'civitai_deleted': bool(get_value('civitai_deleted', False)), + 'skip_metadata_refresh': bool(get_value('skip_metadata_refresh', False)), } license_source: Dict[str, Any] = {} diff --git a/py/services/persistent_model_cache.py b/py/services/persistent_model_cache.py index 9174a64d..46a4cd51 100644 --- a/py/services/persistent_model_cache.py +++ b/py/services/persistent_model_cache.py @@ -52,6 +52,7 @@ class PersistentModelCache: "trained_words", "license_flags", "civitai_deleted", + "skip_metadata_refresh", "exclude", "db_checked", "last_checked_at", @@ -183,6 +184,7 @@ class PersistentModelCache: "tags": tags.get(file_path, []), "civitai": civitai, "civitai_deleted": bool(row["civitai_deleted"]), + "skip_metadata_refresh": bool(row["skip_metadata_refresh"]), "license_flags": int(license_value), } raw_data.append(item) @@ -491,6 +493,7 @@ class PersistentModelCache: "civitai_creator_username": "TEXT", "civitai_model_type": "TEXT", "civitai_deleted": "INTEGER DEFAULT 0", + "skip_metadata_refresh": "INTEGER DEFAULT 0", # Persisting without explicit flags should assume CivitAI's documented defaults (0b111001 == 57). "license_flags": f"INTEGER DEFAULT {DEFAULT_LICENSE_FLAGS}", } @@ -563,6 +566,7 @@ class PersistentModelCache: trained_words_json, int(license_flags), 1 if item.get("civitai_deleted") else 0, + 1 if item.get("skip_metadata_refresh") else 0, 1 if item.get("exclude") else 0, 1 if item.get("db_checked") else 0, float(item.get("last_checked_at") or 0.0), diff --git a/py/services/use_cases/bulk_metadata_refresh_use_case.py b/py/services/use_cases/bulk_metadata_refresh_use_case.py index 11760b4f..590840ca 100644 --- a/py/services/use_cases/bulk_metadata_refresh_use_case.py +++ b/py/services/use_cases/bulk_metadata_refresh_use_case.py @@ -47,6 +47,7 @@ class BulkMetadataRefreshUseCase: model for model in cache.raw_data if model.get("sha256") + and not model.get("skip_metadata_refresh", False) and (not model.get("civitai") or not model["civitai"].get("id")) and not ( # Skip models confirmed not on CivitAI when no need to retry diff --git a/py/utils/models.py b/py/utils/models.py index c55bd29e..af6b91ad 100644 --- a/py/utils/models.py +++ b/py/utils/models.py @@ -25,6 +25,7 @@ class BaseModelMetadata: favorite: bool = False # Whether the model is a favorite exclude: bool = False # Whether to exclude this model from the cache db_checked: bool = False # Whether checked in archive DB + skip_metadata_refresh: bool = False # Whether to skip this model during bulk metadata refresh metadata_source: Optional[str] = None # Last provider that supplied metadata last_checked_at: float = 0 # Last checked timestamp _unknown_fields: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False) # Store unknown fields diff --git a/static/css/base.css b/static/css/base.css index 8d6130a9..ddcac9b9 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -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 { diff --git a/static/css/components/card.css b/static/css/components/card.css index 5372bfcf..dd36ce1f 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -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; +} diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 17ad30e7..80eb511d 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -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; diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index f959424e..fbd14d1a 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -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) { ` : ''} + ${model.skip_metadata_refresh ? ` + + + + ` : ''}
${actionIcons} diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index ed307b0f..211a88de 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -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 */ diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index b020ea12..d2651f8a 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -80,6 +80,12 @@
{{ t('loras.bulkOperations.setContentRating') }}
+
+ {{ t('loras.bulkOperations.skipMetadataRefresh') }} +
+
+ {{ t('loras.bulkOperations.resumeMetadataRefresh') }} +
{{ t('loras.bulkOperations.moveAll') }}