feat(bulk): add bulk favorite/unfavorite toggle with context-sensitive single menu item

Replaces two separate menu items with a single smart item that dynamically
switches between 'Set as Favorite' and 'Remove from Favorites' based on
whether all selected items are already favorited. Shows a count badge
'(3/5)' when only some items are favorited in a mixed selection.

Supports all model types (LoRA, Checkpoint, Embedding) and recipes via
existing per-item save/update API — no backend changes needed.
This commit is contained in:
Will Miao
2026-05-07 09:51:23 +08:00
parent 682e964f89
commit f0a86dbbc0
13 changed files with 197 additions and 5 deletions

View File

@@ -687,6 +687,9 @@
"autoOrganize": "Automatisch organisieren", "autoOrganize": "Automatisch organisieren",
"skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen", "skipMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle überspringen",
"resumeMetadataRefresh": "Metadaten-Aktualisierung für ausgewählte Modelle fortsetzen", "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", "deleteAll": "Ausgewählte löschen",
"downloadMissingLoras": "Fehlende LoRAs herunterladen", "downloadMissingLoras": "Fehlende LoRAs herunterladen",
"clear": "Auswahl löschen", "clear": "Auswahl löschen",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt", "bulkContentRatingSet": "Inhaltsbewertung auf {level} für {count} Modell(e) gesetzt",
"bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen", "bulkContentRatingPartial": "Inhaltsbewertung auf {level} für {success} Modell(e) gesetzt, {failed} fehlgeschlagen",
"bulkContentRatingFailed": "Inhaltsbewertung für ausgewählte Modelle konnte nicht aktualisiert werden", "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...", "bulkUpdatesChecking": "Ausgewählte {type}-Modelle werden auf Updates geprüft...",
"bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar", "bulkUpdatesSuccess": "Updates für {count} ausgewählte {type}-Modelle verfügbar",
"bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden", "bulkUpdatesNone": "Keine Updates für ausgewählte {type}-Modelle gefunden",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "Auto-Organize Selected", "autoOrganize": "Auto-Organize Selected",
"skipMetadataRefresh": "Skip Metadata Refresh for Selected", "skipMetadataRefresh": "Skip Metadata Refresh for Selected",
"resumeMetadataRefresh": "Resume 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", "deleteAll": "Delete Selected",
"downloadMissingLoras": "Download Missing LoRAs", "downloadMissingLoras": "Download Missing LoRAs",
"clear": "Clear Selection", "clear": "Clear Selection",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "Set content rating to {level} 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", "bulkContentRatingPartial": "Set content rating to {level} for {success} model(s), {failed} failed",
"bulkContentRatingFailed": "Failed to update content rating for selected models", "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...", "bulkUpdatesChecking": "Checking selected {type}(s) for updates...",
"bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)", "bulkUpdatesSuccess": "Updates available for {count} selected {type}(s)",
"bulkUpdatesNone": "No updates found for selected {type}(s)", "bulkUpdatesNone": "No updates found for selected {type}(s)",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "Auto-organizar seleccionados", "autoOrganize": "Auto-organizar seleccionados",
"skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados", "skipMetadataRefresh": "Omitir actualización de metadatos para seleccionados",
"resumeMetadataRefresh": "Reanudar 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", "deleteAll": "Eliminar seleccionados",
"downloadMissingLoras": "Descargar LoRAs faltantes", "downloadMissingLoras": "Descargar LoRAs faltantes",
"clear": "Limpiar selección", "clear": "Limpiar selección",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "Clasificación de contenido establecida en {level} 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", "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", "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...", "bulkUpdatesChecking": "Comprobando actualizaciones para {type} seleccionados...",
"bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados", "bulkUpdatesSuccess": "Actualizaciones disponibles para {count} {type} seleccionados",
"bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados", "bulkUpdatesNone": "No se encontraron actualizaciones para los {type} seleccionados",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "Auto-organiser la sélection", "autoOrganize": "Auto-organiser la sélection",
"skipMetadataRefresh": "Ignorer l'actualisation des métadonnées pour 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", "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", "deleteAll": "Supprimer la sélection",
"downloadMissingLoras": "Télécharger les LoRAs manquants", "downloadMissingLoras": "Télécharger les LoRAs manquants",
"clear": "Effacer la sélection", "clear": "Effacer la sélection",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "Classification du contenu définie sur {level} 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)", "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", "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...", "bulkUpdatesChecking": "Vérification des mises à jour pour les {type} sélectionnés...",
"bulkUpdatesSuccess": "Mises à jour disponibles pour {count} {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", "bulkUpdatesNone": "Aucune mise à jour trouvée pour les {type} sélectionnés",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "ארגן אוטומטית נבחרים", "autoOrganize": "ארגן אוטומטית נבחרים",
"skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים", "skipMetadataRefresh": "דילוג על רענון מטא-נתונים לנבחרים",
"resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים", "resumeMetadataRefresh": "המשך רענון מטא-נתונים לנבחרים",
"setFavorite": "הגדר כמועדף",
"setFavoriteCount": "הגדר כמועדף ({favorited}/{total})",
"unfavorite": "הסר ממועדפים",
"deleteAll": "מחק נבחרים", "deleteAll": "מחק נבחרים",
"downloadMissingLoras": "הורדת LoRAs חסרים", "downloadMissingLoras": "הורדת LoRAs חסרים",
"clear": "נקה בחירה", "clear": "נקה בחירה",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים", "bulkContentRatingSet": "דירוג התוכן הוגדר ל-{level} עבור {count} מודלים",
"bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו", "bulkContentRatingPartial": "דירוג התוכן הוגדר ל-{level} עבור {success} מודלים, {failed} נכשלו",
"bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל", "bulkContentRatingFailed": "עדכון דירוג התוכן עבור המודלים שנבחרו נכשל",
"bulkFavoriteUpdating": "מוסיף {count} דגמים למועדפים...",
"bulkUnfavoriteUpdating": "מסיר {count} דגמים ממועדפים...",
"bulkFavoritePartialAdded": "{success} דגמים נוספו למועדפים, {failed} נכשלו",
"bulkFavoritePartialRemoved": "{success} דגמים הוסרו ממועדפים, {failed} נכשלו",
"bulkFavoriteFailed": "עדכון סטטוס מועדפים נכשל",
"bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...", "bulkUpdatesChecking": "בודק עדכונים עבור {type} שנבחרו...",
"bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו", "bulkUpdatesSuccess": "יש עדכונים עבור {count} {type} שנבחרו",
"bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו", "bulkUpdatesNone": "לא נמצאו עדכונים עבור {type} שנבחרו",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "自動整理を実行", "autoOrganize": "自動整理を実行",
"skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ", "skipMetadataRefresh": "選択したモデルのメタデータ更新をスキップ",
"resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開", "resumeMetadataRefresh": "選択したモデルのメタデータ更新を再開",
"setFavorite": "お気に入りに設定",
"setFavoriteCount": "お気に入りに設定 ({favorited}/{total})",
"unfavorite": "お気に入りから削除",
"deleteAll": "選択したものを削除", "deleteAll": "選択したものを削除",
"downloadMissingLoras": "不足している LoRA をダウンロード", "downloadMissingLoras": "不足している LoRA をダウンロード",
"clear": "選択をクリア", "clear": "選択をクリア",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました", "bulkContentRatingSet": "{count} 件のモデルのコンテンツレーティングを {level} に設定しました",
"bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました", "bulkContentRatingPartial": "{success} 件のモデルのコンテンツレーティングを {level} に設定、{failed} 件は失敗しました",
"bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした", "bulkContentRatingFailed": "選択したモデルのコンテンツレーティングを更新できませんでした",
"bulkFavoriteUpdating": "{count} 個のモデルをお気に入りに追加中...",
"bulkUnfavoriteUpdating": "{count} 個のモデルをお気に入りから削除中...",
"bulkFavoritePartialAdded": "{success} 個のモデルをお気に入りに追加、{failed} 個失敗",
"bulkFavoritePartialRemoved": "{success} 個のモデルをお気に入りから削除、{failed} 個失敗",
"bulkFavoriteFailed": "お気に入り状態の更新に失敗しました",
"bulkUpdatesChecking": "選択された{type}の更新を確認しています...", "bulkUpdatesChecking": "選択された{type}の更新を確認しています...",
"bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります", "bulkUpdatesSuccess": "{count} 件の選択された{type}に利用可能な更新があります",
"bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした", "bulkUpdatesNone": "選択された{type}には更新が見つかりませんでした",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "자동 정리 선택", "autoOrganize": "자동 정리 선택",
"skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기", "skipMetadataRefresh": "선택한 모델의 메타데이터 새로고침 건너뛰기",
"resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개", "resumeMetadataRefresh": "선택한 모델의 메타데이터 새로고침 재개",
"setFavorite": "즐겨찾기로 설정",
"setFavoriteCount": "즐겨찾기로 설정 ({favorited}/{total})",
"unfavorite": "즐겨찾기 해제",
"deleteAll": "선택된 항목 삭제", "deleteAll": "선택된 항목 삭제",
"downloadMissingLoras": "누락된 LoRA 다운로드", "downloadMissingLoras": "누락된 LoRA 다운로드",
"clear": "선택 지우기", "clear": "선택 지우기",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다", "bulkContentRatingSet": "{count}개 모델의 콘텐츠 등급을 {level}(으)로 설정했습니다",
"bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다", "bulkContentRatingPartial": "{success}개 모델의 콘텐츠 등급을 {level}(으)로 설정했고, {failed}개는 실패했습니다",
"bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다", "bulkContentRatingFailed": "선택한 모델의 콘텐츠 등급을 업데이트하지 못했습니다",
"bulkFavoriteUpdating": "{count}개 모델을 즐겨찾기에 추가 중...",
"bulkUnfavoriteUpdating": "{count}개 모델을 즐겨찾기에서 제거 중...",
"bulkFavoritePartialAdded": "{success}개 모델을 즐겨찾기에 추가, {failed}개 실패",
"bulkFavoritePartialRemoved": "{success}개 모델을 즐겨찾기에서 제거, {failed}개 실패",
"bulkFavoriteFailed": "즐겨찾기 상태 업데이트 실패",
"bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...", "bulkUpdatesChecking": "선택한 {type}의 업데이트를 확인하는 중...",
"bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다", "bulkUpdatesSuccess": "선택한 {count}개의 {type}에 사용할 수 있는 업데이트가 있습니다",
"bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다", "bulkUpdatesNone": "선택한 {type}에 대한 업데이트가 없습니다",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "Автоматически организовать выбранные", "autoOrganize": "Автоматически организовать выбранные",
"skipMetadataRefresh": "Пропустить обновление метаданных для выбранных", "skipMetadataRefresh": "Пропустить обновление метаданных для выбранных",
"resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных", "resumeMetadataRefresh": "Возобновить обновление метаданных для выбранных",
"setFavorite": "Добавить в избранное",
"setFavoriteCount": "Добавить в избранное ({favorited}/{total})",
"unfavorite": "Удалить из избранного",
"deleteAll": "Удалить выбранные", "deleteAll": "Удалить выбранные",
"downloadMissingLoras": "Скачать отсутствующие LoRAs", "downloadMissingLoras": "Скачать отсутствующие LoRAs",
"clear": "Очистить выбор", "clear": "Очистить выбор",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)", "bulkContentRatingSet": "Рейтинг контента установлен на {level} для {count} модель(ей)",
"bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось", "bulkContentRatingPartial": "Рейтинг контента {level} установлен для {success} модель(ей), {failed} не удалось",
"bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей", "bulkContentRatingFailed": "Не удалось обновить рейтинг контента для выбранных моделей",
"bulkFavoriteUpdating": "Добавление {count} моделей в избранное...",
"bulkUnfavoriteUpdating": "Удаление {count} моделей из избранного...",
"bulkFavoritePartialAdded": "{success} моделей добавлено в избранное, {failed} не удалось",
"bulkFavoritePartialRemoved": "{success} моделей удалено из избранного, {failed} не удалось",
"bulkFavoriteFailed": "Не удалось обновить статус избранного",
"bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...", "bulkUpdatesChecking": "Проверка обновлений для выбранных {type}...",
"bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}", "bulkUpdatesSuccess": "Доступны обновления для {count} выбранных {type}",
"bulkUpdatesNone": "Обновления для выбранных {type} не найдены", "bulkUpdatesNone": "Обновления для выбранных {type} не найдены",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "自动整理所选模型", "autoOrganize": "自动整理所选模型",
"skipMetadataRefresh": "跳过所选模型的元数据刷新", "skipMetadataRefresh": "跳过所选模型的元数据刷新",
"resumeMetadataRefresh": "恢复所选模型的元数据刷新", "resumeMetadataRefresh": "恢复所选模型的元数据刷新",
"setFavorite": "设为收藏",
"setFavoriteCount": "设为收藏 ({favorited}/{total})",
"unfavorite": "取消收藏",
"deleteAll": "删除已选", "deleteAll": "删除已选",
"downloadMissingLoras": "下载缺失的 LoRAs", "downloadMissingLoras": "下载缺失的 LoRAs",
"clear": "清除选择", "clear": "清除选择",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}", "bulkContentRatingSet": "已将 {count} 个模型的内容评级设置为 {level}",
"bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level}{failed} 个失败", "bulkContentRatingPartial": "已将 {success} 个模型的内容评级设置为 {level}{failed} 个失败",
"bulkContentRatingFailed": "未能更新所选模型的内容评级", "bulkContentRatingFailed": "未能更新所选模型的内容评级",
"bulkFavoriteUpdating": "正在将 {count} 个模型添加到收藏...",
"bulkUnfavoriteUpdating": "正在将 {count} 个模型从收藏移除...",
"bulkFavoritePartialAdded": "已将 {success} 个模型添加到收藏,{failed} 个失败",
"bulkFavoritePartialRemoved": "已将 {success} 个模型从收藏移除,{failed} 个失败",
"bulkFavoriteFailed": "更新收藏状态失败",
"bulkUpdatesChecking": "正在检查所选 {type} 的更新...", "bulkUpdatesChecking": "正在检查所选 {type} 的更新...",
"bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新", "bulkUpdatesSuccess": "{count} 个所选 {type} 有可用更新",
"bulkUpdatesNone": "所选 {type} 未发现更新", "bulkUpdatesNone": "所选 {type} 未发现更新",

View File

@@ -687,6 +687,9 @@
"autoOrganize": "自動整理所選模型", "autoOrganize": "自動整理所選模型",
"skipMetadataRefresh": "跳過所選模型的元數據更新", "skipMetadataRefresh": "跳過所選模型的元數據更新",
"resumeMetadataRefresh": "恢復所選模型的元數據更新", "resumeMetadataRefresh": "恢復所選模型的元數據更新",
"setFavorite": "設為收藏",
"setFavoriteCount": "設為收藏 ({favorited}/{total})",
"unfavorite": "取消收藏",
"deleteAll": "刪除所選", "deleteAll": "刪除所選",
"downloadMissingLoras": "下載缺失的 LoRAs", "downloadMissingLoras": "下載缺失的 LoRAs",
"clear": "清除選取", "clear": "清除選取",
@@ -1699,6 +1702,11 @@
"bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}", "bulkContentRatingSet": "已將 {count} 個模型的內容分級設定為 {level}",
"bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level}{failed} 個失敗", "bulkContentRatingPartial": "已將 {success} 個模型的內容分級設定為 {level}{failed} 個失敗",
"bulkContentRatingFailed": "無法更新所選模型的內容分級", "bulkContentRatingFailed": "無法更新所選模型的內容分級",
"bulkFavoriteUpdating": "正在將 {count} 個模型加入收藏...",
"bulkUnfavoriteUpdating": "正在將 {count} 個模型從收藏移除...",
"bulkFavoritePartialAdded": "已將 {success} 個模型加入收藏,{failed} 個失敗",
"bulkFavoritePartialRemoved": "已將 {success} 個模型從收藏移除,{failed} 個失敗",
"bulkFavoriteFailed": "更新收藏狀態失敗",
"bulkUpdatesChecking": "正在檢查所選 {type} 的更新...", "bulkUpdatesChecking": "正在檢查所選 {type} 的更新...",
"bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新", "bulkUpdatesSuccess": "{count} 個所選 {type} 有可用更新",
"bulkUpdatesNone": "所選 {type} 未找到更新", "bulkUpdatesNone": "所選 {type} 未找到更新",

View File

@@ -74,6 +74,34 @@ export class BulkContextMenu extends BaseContextMenu {
if (setContentRatingItem) { if (setContentRatingItem) {
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none'; 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) { if (downloadMissingLorasItem) {
// Only show for recipes page // Only show for recipes page
downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none'; downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none';
@@ -138,6 +166,20 @@ export class BulkContextMenu extends BaseContextMenu {
return count; 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) { showMenu(x, y, card) {
this.updateMenuItemsForModelType(); this.updateMenuItemsForModelType();
this.updateSelectedCountHeader(); this.updateSelectedCountHeader();
@@ -185,6 +227,11 @@ export class BulkContextMenu extends BaseContextMenu {
case 'delete-all': case 'delete-all':
bulkManager.showBulkDeleteModal(); bulkManager.showBulkDeleteModal();
break; break;
case 'set-favorite': {
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
bulkManager.setBulkFavorites(!allFavorited);
break;
}
case 'download-missing-loras': case 'download-missing-loras':
this.handleDownloadMissingLoras(); this.handleDownloadMissingLoras();
break; break;

View File

@@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
import { modalManager } from './ModalManager.js'; import { modalManager } from './ModalManager.js';
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.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 { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js'; import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js'; import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
@@ -41,7 +41,9 @@ export class BulkManager {
autoOrganize: true, autoOrganize: true,
deleteAll: true, deleteAll: true,
setContentRating: true, setContentRating: true,
skipMetadataRefresh: true skipMetadataRefresh: true,
setFavorite: true,
unfavorite: true
}, },
[MODEL_TYPES.EMBEDDING]: { [MODEL_TYPES.EMBEDDING]: {
addTags: true, addTags: true,
@@ -53,7 +55,9 @@ export class BulkManager {
autoOrganize: true, autoOrganize: true,
deleteAll: true, deleteAll: true,
setContentRating: false, setContentRating: false,
skipMetadataRefresh: true skipMetadataRefresh: true,
setFavorite: true,
unfavorite: true
}, },
[MODEL_TYPES.CHECKPOINT]: { [MODEL_TYPES.CHECKPOINT]: {
addTags: true, addTags: true,
@@ -65,7 +69,9 @@ export class BulkManager {
autoOrganize: true, autoOrganize: true,
deleteAll: true, deleteAll: true,
setContentRating: true, setContentRating: true,
skipMetadataRefresh: true skipMetadataRefresh: true,
setFavorite: true,
unfavorite: true
}, },
recipes: { recipes: {
addTags: false, addTags: false,
@@ -77,7 +83,9 @@ export class BulkManager {
autoOrganize: false, autoOrganize: false,
deleteAll: true, deleteAll: true,
setContentRating: false, 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 * Show bulk base model modal
*/ */

View File

@@ -77,6 +77,9 @@
<div class="context-menu-item" data-action="set-base-model"> <div class="context-menu-item" data-action="set-base-model">
<i class="fas fa-layer-group"></i> <span>{{ t('loras.bulkOperations.setBaseModel') }}</span> <i class="fas fa-layer-group"></i> <span>{{ t('loras.bulkOperations.setBaseModel') }}</span>
</div> </div>
<div class="context-menu-item" data-action="set-favorite">
<i class="fas fa-star"></i> <span>{{ t('loras.bulkOperations.setFavorite') }}</span>
</div>
<div class="context-menu-item" data-action="set-content-rating"> <div class="context-menu-item" data-action="set-content-rating">
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span> <i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
</div> </div>