diff --git a/locales/de.json b/locales/de.json index 5e1aa2c9..69b50f6f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -687,6 +687,9 @@ "autoOrganize": "Automatisch organisieren", "skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen", "resumeMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle fortsetzen", + "setFavorite": "Als Favorit setzen", + "setFavoriteCount": "Als Favorit setzen ({favorited}/{total})", + "unfavorite": "Aus Favoriten entfernen", "deleteAll": "Ausgewählte löschen", "downloadMissingLoras": "Fehlende LoRAs herunterladen", "clear": "Auswahl löschen", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt", "bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen", "bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden", + "bulkFavoriteUpdating": "Füge {count} Modell(e) zu Favoriten hinzu...", + "bulkUnfavoriteUpdating": "Entferne {count} Modell(e) aus Favoriten...", + "bulkFavoritePartialAdded": "{success} Modell(e) zu Favoriten hinzugefügt, {failed} fehlgeschlagen", + "bulkFavoritePartialRemoved": "{success} Modell(e) aus Favoriten entfernt, {failed} fehlgeschlagen", + "bulkFavoriteFailed": "Fehler beim Aktualisieren des Favoritenstatus", "bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...", "bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar", "bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden", diff --git a/locales/en.json b/locales/en.json index dd125f02..0a1235e4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -687,6 +687,9 @@ "autoOrganize": "Auto-Organize Selected", "skipMetadataRefresh": "Skip Metadata Refresh for Selected", "resumeMetadataRefresh": "Resume Metadata Refresh for Selected", + "setFavorite": "Set as Favorite", + "setFavoriteCount": "Set as Favorite ({favorited}/{total})", + "unfavorite": "Remove from Favorites", "deleteAll": "Delete Selected", "downloadMissingLoras": "Download Missing LoRAs", "clear": "Clear Selection", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "Set content rating to {level} for {count} model(s)", "bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed", "bulkContentRatingFailed": "Failed to update content rating for selected models", + "bulkFavoriteUpdating": "Adding {count} model(s) to favorites...", + "bulkUnfavoriteUpdating": "Removing {count} model(s) from favorites...", + "bulkFavoritePartialAdded": "Added {success} model(s) to favorites, {failed} failed", + "bulkFavoritePartialRemoved": "Removed {success} model(s) from favorites, {failed} failed", + "bulkFavoriteFailed": "Failed to update favorite status for selected models", "bulkUpdatesChecking": "Checking selected {type}(s) for updates...", "bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)", "bulkUpdatesNone": "No updates found for selected {type}(s)", diff --git a/locales/es.json b/locales/es.json index a22f95d6..571bd7b7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -687,6 +687,9 @@ "autoOrganize": "Auto-organizar seleccionados", "skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados", "resumeMetadataRefresh": "Reanudar actualización de metadatos para seleccionados", + "setFavorite": "Marcar como favorito", + "setFavoriteCount": "Marcar como favorito ({favorited}/{total})", + "unfavorite": "Quitar de favoritos", "deleteAll": "Eliminar seleccionados", "downloadMissingLoras": "Descargar LoRAs faltantes", "clear": "Limpiar selección", @@ -1699,6 +1702,11 @@ "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", "bulkContentRatingFailed": "No se pudo actualizar la clasificación de contenido para los modelos seleccionados", + "bulkFavoriteUpdating": "Añadiendo {count} modelo(s) a favoritos...", + "bulkUnfavoriteUpdating": "Eliminando {count} modelo(s) de favoritos...", + "bulkFavoritePartialAdded": "{success} modelo(s) añadido(s) a favoritos, {failed} fallido(s)", + "bulkFavoritePartialRemoved": "{success} modelo(s) eliminado(s) de favoritos, {failed} fallido(s)", + "bulkFavoriteFailed": "Error al actualizar el estado de favorito", "bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...", "bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados", "bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados", diff --git a/locales/fr.json b/locales/fr.json index f3fe26e8..74c723a4 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -687,6 +687,9 @@ "autoOrganize": "Auto-organiser la sélection", "skipMetadataRefresh": "Ignorer l'actualisation des métadonnées pour la sélection", "resumeMetadataRefresh": "Reprendre l'actualisation des métadonnées pour la sélection", + "setFavorite": "Définir comme favori", + "setFavoriteCount": "Définir comme favori ({favorited}/{total})", + "unfavorite": "Retirer des favoris", "deleteAll": "Supprimer la sélection", "downloadMissingLoras": "Télécharger les LoRAs manquants", "clear": "Effacer la sélection", @@ -1699,6 +1702,11 @@ "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)", "bulkContentRatingFailed": "Impossible de mettre à jour la classification du contenu pour les modèles sélectionnés", + "bulkFavoriteUpdating": "Ajout de {count} modèle(s) aux favoris...", + "bulkUnfavoriteUpdating": "Suppression de {count} modèle(s) des favoris...", + "bulkFavoritePartialAdded": "{success} modèle(s) ajouté(s) aux favoris, {failed} échec(s)", + "bulkFavoritePartialRemoved": "{success} modèle(s) retiré(s) des favoris, {failed} échec(s)", + "bulkFavoriteFailed": "Échec de la mise à jour du statut de favori", "bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...", "bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {type} sélectionnés", "bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés", diff --git a/locales/he.json b/locales/he.json index 85692c19..59875292 100644 --- a/locales/he.json +++ b/locales/he.json @@ -687,6 +687,9 @@ "autoOrganize": "ארגן אוטומטית נבחרים", "skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים", "resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים", + "setFavorite": "הגדר כמועדף", + "setFavoriteCount": "הגדר כמועדף ({favorited}/{total})", + "unfavorite": "הסר ממועדפים", "deleteAll": "מחק נבחרים", "downloadMissingLoras": "הורדת LoRAs חסרים", "clear": "נקה בחירה", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים", "bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו", "bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל", + "bulkFavoriteUpdating": "מוסיף {count} דגמים למועדפים...", + "bulkUnfavoriteUpdating": "מסיר {count} דגמים ממועדפים...", + "bulkFavoritePartialAdded": "{success} דגמים נוספו למועדפים, {failed} נכשלו", + "bulkFavoritePartialRemoved": "{success} דגמים הוסרו ממועדפים, {failed} נכשלו", + "bulkFavoriteFailed": "עדכון סטטוס מועדפים נכשל", "bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...", "bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו", "bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו", diff --git a/locales/ja.json b/locales/ja.json index 2ff5535a..12098f5f 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -687,6 +687,9 @@ "autoOrganize": "自動整理を実行", "skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ", "resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開", + "setFavorite": "お気に入りに設定", + "setFavoriteCount": "お気に入りに設定 ({favorited}/{total})", + "unfavorite": "お気に入りから削除", "deleteAll": "選択したものを削除", "downloadMissingLoras": "不足している LoRA をダウンロード", "clear": "選択をクリア", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました", "bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました", "bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした", + "bulkFavoriteUpdating": "{count} 個のモデルをお気に入りに追加中...", + "bulkUnfavoriteUpdating": "{count} 個のモデルをお気に入りから削除中...", + "bulkFavoritePartialAdded": "{success} 個のモデルをお気に入りに追加、{failed} 個失敗", + "bulkFavoritePartialRemoved": "{success} 個のモデルをお気に入りから削除、{failed} 個失敗", + "bulkFavoriteFailed": "お気に入り状態の更新に失敗しました", "bulkUpdatesChecking": "選択された{type}の更新を確認しています...", "bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります", "bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした", diff --git a/locales/ko.json b/locales/ko.json index 99a60249..add2583d 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -687,6 +687,9 @@ "autoOrganize": "자동 정리 선택", "skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기", "resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개", + "setFavorite": "즐겨찾기로 설정", + "setFavoriteCount": "즐겨찾기로 설정 ({favorited}/{total})", + "unfavorite": "즐겨찾기 해제", "deleteAll": "선택된 항목 삭제", "downloadMissingLoras": "누락된 LoRA 다운로드", "clear": "선택 지우기", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다", "bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다", "bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다", + "bulkFavoriteUpdating": "{count}개 모델을 즐겨찾기에 추가 중...", + "bulkUnfavoriteUpdating": "{count}개 모델을 즐겨찾기에서 제거 중...", + "bulkFavoritePartialAdded": "{success}개 모델을 즐겨찾기에 추가, {failed}개 실패", + "bulkFavoritePartialRemoved": "{success}개 모델을 즐겨찾기에서 제거, {failed}개 실패", + "bulkFavoriteFailed": "즐겨찾기 상태 업데이트 실패", "bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...", "bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다", "bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다", diff --git a/locales/ru.json b/locales/ru.json index c3fe71d8..0f83a9aa 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -687,6 +687,9 @@ "autoOrganize": "Автоматически организовать выбранные", "skipMetadataRefresh": "Пропустить обновление метаданных для выбранных", "resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных", + "setFavorite": "Добавить в избранное", + "setFavoriteCount": "Добавить в избранное ({favorited}/{total})", + "unfavorite": "Удалить из избранного", "deleteAll": "Удалить выбранные", "downloadMissingLoras": "Скачать отсутствующие LoRAs", "clear": "Очистить выбор", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)", "bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось", "bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей", + "bulkFavoriteUpdating": "Добавление {count} моделей в избранное...", + "bulkUnfavoriteUpdating": "Удаление {count} моделей из избранного...", + "bulkFavoritePartialAdded": "{success} моделей добавлено в избранное, {failed} не удалось", + "bulkFavoritePartialRemoved": "{success} моделей удалено из избранного, {failed} не удалось", + "bulkFavoriteFailed": "Не удалось обновить статус избранного", "bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...", "bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}", "bulkUpdatesNone": "Обновления для выбранных {type} не найдены", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 955b8264..6c7f3856 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -687,6 +687,9 @@ "autoOrganize": "自动整理所选模型", "skipMetadataRefresh": "跳过所选模型的元数据刷新", "resumeMetadataRefresh": "恢复所选模型的元数据刷新", + "setFavorite": "设为收藏", + "setFavoriteCount": "设为收藏 ({favorited}/{total})", + "unfavorite": "取消收藏", "deleteAll": "删除已选", "downloadMissingLoras": "下载缺失的 LoRAs", "clear": "清除选择", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}", "bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level},{failed} 个失败", "bulkContentRatingFailed": "未能更新所选模型的内容评级", + "bulkFavoriteUpdating": "正在将 {count} 个模型添加到收藏...", + "bulkUnfavoriteUpdating": "正在将 {count} 个模型从收藏移除...", + "bulkFavoritePartialAdded": "已将 {success} 个模型添加到收藏,{failed} 个失败", + "bulkFavoritePartialRemoved": "已将 {success} 个模型从收藏移除,{failed} 个失败", + "bulkFavoriteFailed": "更新收藏状态失败", "bulkUpdatesChecking": "正在检查所选 {type} 的更新...", "bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新", "bulkUpdatesNone": "所选 {type} 未发现更新", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 52a53a41..07f070f9 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -687,6 +687,9 @@ "autoOrganize": "自動整理所選模型", "skipMetadataRefresh": "跳過所選模型的元數據更新", "resumeMetadataRefresh": "恢復所選模型的元數據更新", + "setFavorite": "設為收藏", + "setFavoriteCount": "設為收藏 ({favorited}/{total})", + "unfavorite": "取消收藏", "deleteAll": "刪除所選", "downloadMissingLoras": "下載缺失的 LoRAs", "clear": "清除選取", @@ -1699,6 +1702,11 @@ "bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}", "bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level},{failed} 個失敗", "bulkContentRatingFailed": "無法更新所選模型的內容分級", + "bulkFavoriteUpdating": "正在將 {count} 個模型加入收藏...", + "bulkUnfavoriteUpdating": "正在將 {count} 個模型從收藏移除...", + "bulkFavoritePartialAdded": "已將 {success} 個模型加入收藏,{failed} 個失敗", + "bulkFavoritePartialRemoved": "已將 {success} 個模型從收藏移除,{failed} 個失敗", + "bulkFavoriteFailed": "更新收藏狀態失敗", "bulkUpdatesChecking": "正在檢查所選 {type} 的更新...", "bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新", "bulkUpdatesNone": "所選 {type} 未找到更新", diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 84995051..993af247 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -74,6 +74,34 @@ export class BulkContextMenu extends BaseContextMenu { if (setContentRatingItem) { setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none'; } + + const setFavoriteItem = this.menu.querySelector('[data-action="set-favorite"]'); + + if (setFavoriteItem && config.setFavorite) { + setFavoriteItem.style.display = 'flex'; + + const total = state.selectedModels.size; + const favoritedCount = this.countFavoritedInSelection(); + const allFavorited = total > 0 && favoritedCount === total; + + const icon = setFavoriteItem.querySelector('i'); + const label = setFavoriteItem.querySelector('span'); + + if (allFavorited) { + if (icon) { icon.className = 'far fa-star'; } + if (label) { label.textContent = translate('loras.bulkOperations.unfavorite'); } + } else { + if (icon) { icon.className = 'fas fa-star'; } + if (label) { + label.textContent = favoritedCount > 0 + ? translate('loras.bulkOperations.setFavoriteCount', { favorited: favoritedCount, total }) + : translate('loras.bulkOperations.setFavorite'); + } + } + } else if (setFavoriteItem) { + setFavoriteItem.style.display = 'none'; + } + if (downloadMissingLorasItem) { // Only show for recipes page downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none'; @@ -138,6 +166,20 @@ export class BulkContextMenu extends BaseContextMenu { return count; } + countFavoritedInSelection() { + let count = 0; + for (const filePath of state.selectedModels) { + const escapedPath = window.CSS && typeof window.CSS.escape === 'function' + ? window.CSS.escape(filePath) + : filePath.replace(/["\\]/g, '\\$&'); + const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`); + if (card && card.dataset.favorite === 'true') { + count++; + } + } + return count; + } + showMenu(x, y, card) { this.updateMenuItemsForModelType(); this.updateSelectedCountHeader(); @@ -185,6 +227,11 @@ export class BulkContextMenu extends BaseContextMenu { case 'delete-all': bulkManager.showBulkDeleteModal(); break; + case 'set-favorite': { + const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size; + bulkManager.setBulkFavorites(!allFavorited); + break; + } case 'download-missing-loras': this.handleDownloadMissingLoras(); break; diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index fa44914d..93c3ed42 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; -import { RecipeSidebarApiClient } from '../api/recipeApi.js'; +import { RecipeSidebarApiClient, updateRecipeMetadata } from '../api/recipeApi.js'; import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js'; import { BASE_MODEL_CATEGORIES } from '../utils/constants.js'; import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js'; @@ -41,7 +41,9 @@ export class BulkManager { autoOrganize: true, deleteAll: true, setContentRating: true, - skipMetadataRefresh: true + skipMetadataRefresh: true, + setFavorite: true, + unfavorite: true }, [MODEL_TYPES.EMBEDDING]: { addTags: true, @@ -53,7 +55,9 @@ export class BulkManager { autoOrganize: true, deleteAll: true, setContentRating: false, - skipMetadataRefresh: true + skipMetadataRefresh: true, + setFavorite: true, + unfavorite: true }, [MODEL_TYPES.CHECKPOINT]: { addTags: true, @@ -65,7 +69,9 @@ export class BulkManager { autoOrganize: true, deleteAll: true, setContentRating: true, - skipMetadataRefresh: true + skipMetadataRefresh: true, + setFavorite: true, + unfavorite: true }, recipes: { addTags: false, @@ -77,7 +83,9 @@ export class BulkManager { autoOrganize: false, deleteAll: true, setContentRating: false, - skipMetadataRefresh: false + skipMetadataRefresh: false, + setFavorite: true, + unfavorite: true } }; @@ -1090,6 +1098,60 @@ export class BulkManager { } } + async setBulkFavorites(value) { + if (state.selectedModels.size === 0) { + showToast('toast.models.noModelsSelected', {}, 'warning'); + return; + } + + const totalCount = state.selectedModels.size; + const isRecipesPage = state.currentPageType === 'recipes'; + + state.loadingManager.showSimpleLoading( + translate(value ? 'toast.models.bulkFavoriteUpdating' : 'toast.models.bulkUnfavoriteUpdating', { count: totalCount }) + ); + let cancelled = false; + state.loadingManager.showCancelButton(() => { + cancelled = true; + }); + + let successCount = 0; + let failureCount = 0; + + try { + for (const filePath of state.selectedModels) { + if (cancelled) { + showToast('toast.api.operationCancelled', {}, 'info'); + break; + } + try { + if (isRecipesPage) { + await updateRecipeMetadata(filePath, { favorite: value }); + } else { + const apiClient = getModelApiClient(); + await apiClient.saveModelMetadata(filePath, { favorite: value }); + } + successCount++; + } catch (error) { + failureCount++; + console.error(`Failed to set favorite=${value} for ${filePath}:`, error); + } + } + } finally { + state.loadingManager?.hide?.(); + } + + if (successCount === totalCount) { + const toastKey = value ? 'modelCard.favorites.added' : 'modelCard.favorites.removed'; + showToast(toastKey, {}, 'success'); + } else if (successCount > 0) { + const toastKey = value ? 'toast.models.bulkFavoritePartialAdded' : 'toast.models.bulkFavoritePartialRemoved'; + showToast(toastKey, { success: successCount, failed: failureCount }, 'warning'); + } else { + showToast('toast.models.bulkFavoriteFailed', {}, 'error'); + } + } + /** * Show bulk base model modal */ diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index 679acff7..c0de9c5a 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -77,6 +77,9 @@
+