mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
@@ -194,7 +194,7 @@
|
|||||||
"displayDensity": "Anzeige-Dichte",
|
"displayDensity": "Anzeige-Dichte",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
"default": "Standard",
|
"default": "Standard",
|
||||||
"medium": "Mittel",
|
"medium": "Mittel",
|
||||||
"compact": "Kompakt"
|
"compact": "Kompakt"
|
||||||
},
|
},
|
||||||
"displayDensityHelp": "Wählen Sie, wie viele Karten pro Zeile angezeigt werden sollen:",
|
"displayDensityHelp": "Wählen Sie, wie viele Karten pro Zeile angezeigt werden sollen:",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"templateOptions": {
|
"templateOptions": {
|
||||||
"flatStructure": "Flache Struktur",
|
"flatStructure": "Flache Struktur",
|
||||||
"byBaseModel": "Nach Basis-Modell",
|
"byBaseModel": "Nach Basis-Modell",
|
||||||
"byAuthor": "Nach Autor",
|
"byAuthor": "Nach Autor",
|
||||||
"byFirstTag": "Nach erstem Tag",
|
"byFirstTag": "Nach erstem Tag",
|
||||||
"baseModelFirstTag": "Basis-Modell + Erster Tag",
|
"baseModelFirstTag": "Basis-Modell + Erster Tag",
|
||||||
"baseModelAuthor": "Basis-Modell + Autor",
|
"baseModelAuthor": "Basis-Modell + Autor",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"customTemplatePlaceholder": "Benutzerdefinierte Vorlage eingeben (z.B. {base_model}/{author}/{first_tag})",
|
"customTemplatePlaceholder": "Benutzerdefinierte Vorlage eingeben (z.B. {base_model}/{author}/{first_tag})",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
"checkpoint": "Checkpoint",
|
"checkpoint": "Checkpoint",
|
||||||
"embedding": "Embedding"
|
"embedding": "Embedding"
|
||||||
},
|
},
|
||||||
"baseModelPathMappings": "Basis-Modell-Pfad-Zuordnungen",
|
"baseModelPathMappings": "Basis-Modell-Pfad-Zuordnungen",
|
||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count} ausgewählt",
|
"selected": "{count} ausgewählt",
|
||||||
"selectedSuffix": "ausgewählt",
|
"selectedSuffix": "ausgewählt",
|
||||||
"viewSelected": "Klicken Sie, um ausgewählte Elemente anzuzeigen",
|
"viewSelected": "Klicken Sie, um ausgewählte Elemente anzuzeigen",
|
||||||
|
"addTags": "Tags hinzufügen",
|
||||||
"sendToWorkflow": "An Workflow senden",
|
"sendToWorkflow": "An Workflow senden",
|
||||||
"copyAll": "Alle kopieren",
|
"copyAll": "Alle kopieren",
|
||||||
"refreshAll": "Alle aktualisieren",
|
"refreshAll": "Alle aktualisieren",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "Modelle werden dauerhaft gelöscht.",
|
"countMessage": "Modelle werden dauerhaft gelöscht.",
|
||||||
"action": "Alle löschen"
|
"action": "Alle löschen"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "Tags zu mehreren Modellen hinzufügen",
|
||||||
|
"description": "Tags hinzufügen zu",
|
||||||
|
"models": "Modelle",
|
||||||
|
"tagsToAdd": "Hinzugefügte Tags",
|
||||||
|
"placeholder": "Tag eingeben und Enter drücken...",
|
||||||
|
"appendTags": "Tags anhängen",
|
||||||
|
"replaceTags": "Tags ersetzen",
|
||||||
|
"saveChanges": "Änderungen speichern"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "Lokale Beispielbilder",
|
"title": "Lokale Beispielbilder",
|
||||||
"message": "Keine lokalen Beispielbilder für dieses Modell gefunden. Ansichtsoptionen:",
|
"message": "Keine lokalen Beispielbilder für dieses Modell gefunden. Ansichtsoptionen:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "Diese Gruppe wurde bereits verifiziert",
|
"verificationAlreadyDone": "Diese Gruppe wurde bereits verifiziert",
|
||||||
"verificationCompleteMismatch": "Verifikation abgeschlossen. {count} Datei(en) haben unterschiedliche tatsächliche Hashes.",
|
"verificationCompleteMismatch": "Verifikation abgeschlossen. {count} Datei(en) haben unterschiedliche tatsächliche Hashes.",
|
||||||
"verificationCompleteSuccess": "Verifikation abgeschlossen. Alle Dateien sind bestätigte Duplikate.",
|
"verificationCompleteSuccess": "Verifikation abgeschlossen. Alle Dateien sind bestätigte Duplikate.",
|
||||||
"verificationFailed": "Fehler beim Verifizieren der Hashes: {message}"
|
"verificationFailed": "Fehler beim Verifizieren der Hashes: {message}",
|
||||||
|
"noTagsToAdd": "Keine Tags zum Hinzufügen",
|
||||||
|
"tagsAddedSuccessfully": "Erfolgreich {tagCount} Tag(s) zu {count} {type}(s) hinzugefügt",
|
||||||
|
"tagsReplacedSuccessfully": "Tags für {count} {type}(s) erfolgreich durch {tagCount} Tag(s) ersetzt",
|
||||||
|
"tagsAddFailed": "Fehler beim Hinzufügen von Tags zu {count} Modell(en)",
|
||||||
|
"tagsReplaceFailed": "Fehler beim Ersetzen von Tags für {count} Modell(e)",
|
||||||
|
"bulkTagsAddFailed": "Fehler beim Hinzufügen von Tags zu Modellen",
|
||||||
|
"bulkTagsReplaceFailed": "Fehler beim Ersetzen von Tags für Modelle"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "Mindestens eine Suchoption muss ausgewählt werden"
|
"atLeastOneOption": "Mindestens eine Suchoption muss ausgewählt werden"
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count} selected",
|
"selected": "{count} selected",
|
||||||
"selectedSuffix": "selected",
|
"selectedSuffix": "selected",
|
||||||
"viewSelected": "Click to view selected items",
|
"viewSelected": "Click to view selected items",
|
||||||
|
"addTags": "Add Tags",
|
||||||
"sendToWorkflow": "Send to Workflow",
|
"sendToWorkflow": "Send to Workflow",
|
||||||
"copyAll": "Copy All",
|
"copyAll": "Copy All",
|
||||||
"refreshAll": "Refresh All",
|
"refreshAll": "Refresh All",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "models will be permanently deleted.",
|
"countMessage": "models will be permanently deleted.",
|
||||||
"action": "Delete All"
|
"action": "Delete All"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "Add Tags to Multiple Models",
|
||||||
|
"description": "Add tags to",
|
||||||
|
"models": "models",
|
||||||
|
"tagsToAdd": "Tags to Add",
|
||||||
|
"placeholder": "Enter tag and press Enter...",
|
||||||
|
"appendTags": "Append Tags",
|
||||||
|
"replaceTags": "Replace Tags",
|
||||||
|
"saveChanges": "Save changes"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "Local Example Images",
|
"title": "Local Example Images",
|
||||||
"message": "No local example images found for this model. View options:",
|
"message": "No local example images found for this model. View options:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "This group has already been verified",
|
"verificationAlreadyDone": "This group has already been verified",
|
||||||
"verificationCompleteMismatch": "Verification complete. {count} file(s) have different actual hashes.",
|
"verificationCompleteMismatch": "Verification complete. {count} file(s) have different actual hashes.",
|
||||||
"verificationCompleteSuccess": "Verification complete. All files are confirmed duplicates.",
|
"verificationCompleteSuccess": "Verification complete. All files are confirmed duplicates.",
|
||||||
"verificationFailed": "Failed to verify hashes: {message}"
|
"verificationFailed": "Failed to verify hashes: {message}",
|
||||||
|
"noTagsToAdd": "No tags to add",
|
||||||
|
"tagsAddedSuccessfully": "Successfully added {tagCount} tag(s) to {count} {type}(s)",
|
||||||
|
"tagsReplacedSuccessfully": "Successfully replaced tags for {count} {type}(s) with {tagCount} tag(s)",
|
||||||
|
"tagsAddFailed": "Failed to add tags to {count} model(s)",
|
||||||
|
"tagsReplaceFailed": "Failed to replace tags for {count} model(s)",
|
||||||
|
"bulkTagsAddFailed": "Failed to add tags to models",
|
||||||
|
"bulkTagsReplaceFailed": "Failed to replace tags for models"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "At least one search option must be selected"
|
"atLeastOneOption": "At least one search option must be selected"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
"displayDensity": "Densidad de visualización",
|
"displayDensity": "Densidad de visualización",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
"default": "Predeterminado",
|
"default": "Predeterminado",
|
||||||
"medium": "Medio",
|
"medium": "Medio",
|
||||||
"compact": "Compacto"
|
"compact": "Compacto"
|
||||||
},
|
},
|
||||||
"displayDensityHelp": "Elige cuántas tarjetas mostrar por fila:",
|
"displayDensityHelp": "Elige cuántas tarjetas mostrar por fila:",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"templateOptions": {
|
"templateOptions": {
|
||||||
"flatStructure": "Estructura plana",
|
"flatStructure": "Estructura plana",
|
||||||
"byBaseModel": "Por modelo base",
|
"byBaseModel": "Por modelo base",
|
||||||
"byAuthor": "Por autor",
|
"byAuthor": "Por autor",
|
||||||
"byFirstTag": "Por primera etiqueta",
|
"byFirstTag": "Por primera etiqueta",
|
||||||
"baseModelFirstTag": "Modelo base + primera etiqueta",
|
"baseModelFirstTag": "Modelo base + primera etiqueta",
|
||||||
"baseModelAuthor": "Modelo base + autor",
|
"baseModelAuthor": "Modelo base + autor",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"customTemplatePlaceholder": "Introduce plantilla personalizada (ej., {base_model}/{author}/{first_tag})",
|
"customTemplatePlaceholder": "Introduce plantilla personalizada (ej., {base_model}/{author}/{first_tag})",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
"checkpoint": "Checkpoint",
|
"checkpoint": "Checkpoint",
|
||||||
"embedding": "Embedding"
|
"embedding": "Embedding"
|
||||||
},
|
},
|
||||||
"baseModelPathMappings": "Mapeos de rutas de modelo base",
|
"baseModelPathMappings": "Mapeos de rutas de modelo base",
|
||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count} seleccionados",
|
"selected": "{count} seleccionados",
|
||||||
"selectedSuffix": "seleccionados",
|
"selectedSuffix": "seleccionados",
|
||||||
"viewSelected": "Clic para ver elementos seleccionados",
|
"viewSelected": "Clic para ver elementos seleccionados",
|
||||||
|
"addTags": "Añadir etiquetas",
|
||||||
"sendToWorkflow": "Enviar al flujo de trabajo",
|
"sendToWorkflow": "Enviar al flujo de trabajo",
|
||||||
"copyAll": "Copiar todo",
|
"copyAll": "Copiar todo",
|
||||||
"refreshAll": "Actualizar todo",
|
"refreshAll": "Actualizar todo",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "modelos serán eliminados permanentemente.",
|
"countMessage": "modelos serán eliminados permanentemente.",
|
||||||
"action": "Eliminar todo"
|
"action": "Eliminar todo"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "Añadir etiquetas a múltiples modelos",
|
||||||
|
"description": "Añadir etiquetas a",
|
||||||
|
"models": "modelos",
|
||||||
|
"tagsToAdd": "Etiquetas a añadir",
|
||||||
|
"placeholder": "Introduce una etiqueta y presiona Enter...",
|
||||||
|
"appendTags": "Añadir etiquetas",
|
||||||
|
"replaceTags": "Reemplazar etiquetas",
|
||||||
|
"saveChanges": "Guardar cambios"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "Imágenes de ejemplo locales",
|
"title": "Imágenes de ejemplo locales",
|
||||||
"message": "No se encontraron imágenes de ejemplo locales para este modelo. Opciones de visualización:",
|
"message": "No se encontraron imágenes de ejemplo locales para este modelo. Opciones de visualización:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "Este grupo ya ha sido verificado",
|
"verificationAlreadyDone": "Este grupo ya ha sido verificado",
|
||||||
"verificationCompleteMismatch": "Verificación completa. {count} archivo(s) tienen hashes reales diferentes.",
|
"verificationCompleteMismatch": "Verificación completa. {count} archivo(s) tienen hashes reales diferentes.",
|
||||||
"verificationCompleteSuccess": "Verificación completa. Todos los archivos son confirmados duplicados.",
|
"verificationCompleteSuccess": "Verificación completa. Todos los archivos son confirmados duplicados.",
|
||||||
"verificationFailed": "Error al verificar hashes: {message}"
|
"verificationFailed": "Error al verificar hashes: {message}",
|
||||||
|
"noTagsToAdd": "No hay etiquetas para añadir",
|
||||||
|
"tagsAddedSuccessfully": "Se añadieron exitosamente {tagCount} etiqueta(s) a {count} {type}(s)",
|
||||||
|
"tagsReplacedSuccessfully": "Se reemplazaron exitosamente las etiquetas de {count} {type}(s) con {tagCount} etiqueta(s)",
|
||||||
|
"tagsAddFailed": "Error al añadir etiquetas a {count} modelo(s)",
|
||||||
|
"tagsReplaceFailed": "Error al reemplazar etiquetas para {count} modelo(s)",
|
||||||
|
"bulkTagsAddFailed": "Error al añadir etiquetas a los modelos",
|
||||||
|
"bulkTagsReplaceFailed": "Error al reemplazar etiquetas para los modelos"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "Al menos una opción de búsqueda debe estar seleccionada"
|
"atLeastOneOption": "Al menos una opción de búsqueda debe estar seleccionada"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
"displayDensity": "Densité d'affichage",
|
"displayDensity": "Densité d'affichage",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
"default": "Par défaut",
|
"default": "Par défaut",
|
||||||
"medium": "Moyen",
|
"medium": "Moyen",
|
||||||
"compact": "Compact"
|
"compact": "Compact"
|
||||||
},
|
},
|
||||||
"displayDensityHelp": "Choisissez combien de cartes afficher par ligne :",
|
"displayDensityHelp": "Choisissez combien de cartes afficher par ligne :",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"templateOptions": {
|
"templateOptions": {
|
||||||
"flatStructure": "Structure plate",
|
"flatStructure": "Structure plate",
|
||||||
"byBaseModel": "Par modèle de base",
|
"byBaseModel": "Par modèle de base",
|
||||||
"byAuthor": "Par auteur",
|
"byAuthor": "Par auteur",
|
||||||
"byFirstTag": "Par premier tag",
|
"byFirstTag": "Par premier tag",
|
||||||
"baseModelFirstTag": "Modèle de base + Premier tag",
|
"baseModelFirstTag": "Modèle de base + Premier tag",
|
||||||
"baseModelAuthor": "Modèle de base + Auteur",
|
"baseModelAuthor": "Modèle de base + Auteur",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"customTemplatePlaceholder": "Entrez un modèle personnalisé (ex: {base_model}/{author}/{first_tag})",
|
"customTemplatePlaceholder": "Entrez un modèle personnalisé (ex: {base_model}/{author}/{first_tag})",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
"checkpoint": "Checkpoint",
|
"checkpoint": "Checkpoint",
|
||||||
"embedding": "Embedding"
|
"embedding": "Embedding"
|
||||||
},
|
},
|
||||||
"baseModelPathMappings": "Mappages de chemin de modèle de base",
|
"baseModelPathMappings": "Mappages de chemin de modèle de base",
|
||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count} sélectionné(s)",
|
"selected": "{count} sélectionné(s)",
|
||||||
"selectedSuffix": "sélectionné(s)",
|
"selectedSuffix": "sélectionné(s)",
|
||||||
"viewSelected": "Cliquez pour voir les éléments sélectionnés",
|
"viewSelected": "Cliquez pour voir les éléments sélectionnés",
|
||||||
|
"addTags": "Ajouter des tags",
|
||||||
"sendToWorkflow": "Envoyer vers le workflow",
|
"sendToWorkflow": "Envoyer vers le workflow",
|
||||||
"copyAll": "Tout copier",
|
"copyAll": "Tout copier",
|
||||||
"refreshAll": "Tout actualiser",
|
"refreshAll": "Tout actualiser",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "modèles seront définitivement supprimés.",
|
"countMessage": "modèles seront définitivement supprimés.",
|
||||||
"action": "Tout supprimer"
|
"action": "Tout supprimer"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "Ajouter des tags à plusieurs modèles",
|
||||||
|
"description": "Ajouter des tags à",
|
||||||
|
"models": "modèles",
|
||||||
|
"tagsToAdd": "Tags à ajouter",
|
||||||
|
"placeholder": "Entrez un tag et appuyez sur Entrée...",
|
||||||
|
"appendTags": "Ajouter les tags",
|
||||||
|
"replaceTags": "Remplacer les tags",
|
||||||
|
"saveChanges": "Enregistrer les modifications"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "Images d'exemple locales",
|
"title": "Images d'exemple locales",
|
||||||
"message": "Aucune image d'exemple locale trouvée pour ce modèle. Options d'affichage :",
|
"message": "Aucune image d'exemple locale trouvée pour ce modèle. Options d'affichage :",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "Ce groupe a déjà été vérifié",
|
"verificationAlreadyDone": "Ce groupe a déjà été vérifié",
|
||||||
"verificationCompleteMismatch": "Vérification terminée. {count} fichier(s) ont des hash différents.",
|
"verificationCompleteMismatch": "Vérification terminée. {count} fichier(s) ont des hash différents.",
|
||||||
"verificationCompleteSuccess": "Vérification terminée. Tous les fichiers sont confirmés comme doublons.",
|
"verificationCompleteSuccess": "Vérification terminée. Tous les fichiers sont confirmés comme doublons.",
|
||||||
"verificationFailed": "Échec de la vérification des hash : {message}"
|
"verificationFailed": "Échec de la vérification des hash : {message}",
|
||||||
|
"noTagsToAdd": "Aucun tag à ajouter",
|
||||||
|
"tagsAddedSuccessfully": "{tagCount} tag(s) ajouté(s) avec succès à {count} {type}(s)",
|
||||||
|
"tagsReplacedSuccessfully": "Tags remplacés avec succès pour {count} {type}(s) avec {tagCount} tag(s)",
|
||||||
|
"tagsAddFailed": "Échec de l'ajout des tags à {count} modèle(s)",
|
||||||
|
"tagsReplaceFailed": "Échec du remplacement des tags pour {count} modèle(s)",
|
||||||
|
"bulkTagsAddFailed": "Échec de l'ajout des tags aux modèles",
|
||||||
|
"bulkTagsReplaceFailed": "Échec du remplacement des tags pour les modèles"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "Au moins une option de recherche doit être sélectionnée"
|
"atLeastOneOption": "Au moins une option de recherche doit être sélectionnée"
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count} 選択中",
|
"selected": "{count} 選択中",
|
||||||
"selectedSuffix": "選択中",
|
"selectedSuffix": "選択中",
|
||||||
"viewSelected": "選択したアイテムを表示するにはクリック",
|
"viewSelected": "選択したアイテムを表示するにはクリック",
|
||||||
|
"addTags": "タグを追加",
|
||||||
"sendToWorkflow": "ワークフローに送信",
|
"sendToWorkflow": "ワークフローに送信",
|
||||||
"copyAll": "すべてコピー",
|
"copyAll": "すべてコピー",
|
||||||
"refreshAll": "すべて更新",
|
"refreshAll": "すべて更新",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "モデルが完全に削除されます。",
|
"countMessage": "モデルが完全に削除されます。",
|
||||||
"action": "すべて削除"
|
"action": "すべて削除"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "複数モデルにタグを追加",
|
||||||
|
"description": "タグを追加するモデル:",
|
||||||
|
"models": "モデル",
|
||||||
|
"tagsToAdd": "追加するタグ",
|
||||||
|
"placeholder": "タグを入力してEnterを押してください...",
|
||||||
|
"appendTags": "タグを追加",
|
||||||
|
"replaceTags": "タグを置換",
|
||||||
|
"saveChanges": "変更を保存"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "ローカル例画像",
|
"title": "ローカル例画像",
|
||||||
"message": "このモデルのローカル例画像が見つかりませんでした。表示オプション:",
|
"message": "このモデルのローカル例画像が見つかりませんでした。表示オプション:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "このグループは既に検証済みです",
|
"verificationAlreadyDone": "このグループは既に検証済みです",
|
||||||
"verificationCompleteMismatch": "検証完了。{count} ファイルの実際のハッシュが異なります。",
|
"verificationCompleteMismatch": "検証完了。{count} ファイルの実際のハッシュが異なります。",
|
||||||
"verificationCompleteSuccess": "検証完了。すべてのファイルが重複であることが確認されました。",
|
"verificationCompleteSuccess": "検証完了。すべてのファイルが重複であることが確認されました。",
|
||||||
"verificationFailed": "ハッシュの検証に失敗しました:{message}"
|
"verificationFailed": "ハッシュの検証に失敗しました:{message}",
|
||||||
|
"noTagsToAdd": "追加するタグがありません",
|
||||||
|
"tagsAddedSuccessfully": "{count} {type} に {tagCount} 個のタグを追加しました",
|
||||||
|
"tagsReplacedSuccessfully": "{count} {type} のタグを {tagCount} 個に置換しました",
|
||||||
|
"tagsAddFailed": "{count} モデルへのタグ追加に失敗しました",
|
||||||
|
"tagsReplaceFailed": "{count} モデルのタグ置換に失敗しました",
|
||||||
|
"bulkTagsAddFailed": "モデルへのタグ追加に失敗しました",
|
||||||
|
"bulkTagsReplaceFailed": "モデルのタグ置換に失敗しました"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "少なくとも1つの検索オプションを選択する必要があります"
|
"atLeastOneOption": "少なくとも1つの検索オプションを選択する必要があります"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
"displayDensity": "표시 밀도",
|
"displayDensity": "표시 밀도",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
"default": "기본",
|
"default": "기본",
|
||||||
"medium": "중간",
|
"medium": "중간",
|
||||||
"compact": "조밀"
|
"compact": "조밀"
|
||||||
},
|
},
|
||||||
"displayDensityHelp": "한 줄에 표시할 카드 수를 선택하세요:",
|
"displayDensityHelp": "한 줄에 표시할 카드 수를 선택하세요:",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"templateOptions": {
|
"templateOptions": {
|
||||||
"flatStructure": "플랫 구조",
|
"flatStructure": "플랫 구조",
|
||||||
"byBaseModel": "베이스 모델별",
|
"byBaseModel": "베이스 모델별",
|
||||||
"byAuthor": "제작자별",
|
"byAuthor": "제작자별",
|
||||||
"byFirstTag": "첫 번째 태그별",
|
"byFirstTag": "첫 번째 태그별",
|
||||||
"baseModelFirstTag": "베이스 모델 + 첫 번째 태그",
|
"baseModelFirstTag": "베이스 모델 + 첫 번째 태그",
|
||||||
"baseModelAuthor": "베이스 모델 + 제작자",
|
"baseModelAuthor": "베이스 모델 + 제작자",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"customTemplatePlaceholder": "사용자 정의 템플릿 입력 (예: {base_model}/{author}/{first_tag})",
|
"customTemplatePlaceholder": "사용자 정의 템플릿 입력 (예: {base_model}/{author}/{first_tag})",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
"checkpoint": "Checkpoint",
|
"checkpoint": "Checkpoint",
|
||||||
"embedding": "Embedding"
|
"embedding": "Embedding"
|
||||||
},
|
},
|
||||||
"baseModelPathMappings": "베이스 모델 경로 매핑",
|
"baseModelPathMappings": "베이스 모델 경로 매핑",
|
||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "{count}개 선택됨",
|
"selected": "{count}개 선택됨",
|
||||||
"selectedSuffix": "개 선택됨",
|
"selectedSuffix": "개 선택됨",
|
||||||
"viewSelected": "선택된 항목 보기",
|
"viewSelected": "선택된 항목 보기",
|
||||||
|
"addTags": "태그 추가",
|
||||||
"sendToWorkflow": "워크플로로 전송",
|
"sendToWorkflow": "워크플로로 전송",
|
||||||
"copyAll": "모두 복사",
|
"copyAll": "모두 복사",
|
||||||
"refreshAll": "모두 새로고침",
|
"refreshAll": "모두 새로고침",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "개의 모델이 영구적으로 삭제됩니다.",
|
"countMessage": "개의 모델이 영구적으로 삭제됩니다.",
|
||||||
"action": "모두 삭제"
|
"action": "모두 삭제"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "여러 모델에 태그 추가",
|
||||||
|
"description": "다음에 태그를 추가합니다:",
|
||||||
|
"models": "모델",
|
||||||
|
"tagsToAdd": "추가할 태그",
|
||||||
|
"placeholder": "태그를 입력하고 Enter를 누르세요...",
|
||||||
|
"appendTags": "태그 추가",
|
||||||
|
"replaceTags": "태그 교체",
|
||||||
|
"saveChanges": "변경사항 저장"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "로컬 예시 이미지",
|
"title": "로컬 예시 이미지",
|
||||||
"message": "이 모델의 로컬 예시 이미지를 찾을 수 없습니다. 보기 옵션:",
|
"message": "이 모델의 로컬 예시 이미지를 찾을 수 없습니다. 보기 옵션:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "이 그룹은 이미 검증되었습니다",
|
"verificationAlreadyDone": "이 그룹은 이미 검증되었습니다",
|
||||||
"verificationCompleteMismatch": "검증 완료. {count}개 파일의 실제 해시가 다릅니다.",
|
"verificationCompleteMismatch": "검증 완료. {count}개 파일의 실제 해시가 다릅니다.",
|
||||||
"verificationCompleteSuccess": "검증 완료. 모든 파일이 중복임을 확인했습니다.",
|
"verificationCompleteSuccess": "검증 완료. 모든 파일이 중복임을 확인했습니다.",
|
||||||
"verificationFailed": "해시 검증 실패: {message}"
|
"verificationFailed": "해시 검증 실패: {message}",
|
||||||
|
"noTagsToAdd": "추가할 태그가 없습니다",
|
||||||
|
"tagsAddedSuccessfully": "{count}개의 {type}에 {tagCount}개의 태그가 성공적으로 추가되었습니다",
|
||||||
|
"tagsReplacedSuccessfully": "{count}개의 {type}의 태그가 {tagCount}개의 태그로 성공적으로 교체되었습니다",
|
||||||
|
"tagsAddFailed": "{count}개의 모델에 태그 추가에 실패했습니다",
|
||||||
|
"tagsReplaceFailed": "{count}개의 모델의 태그 교체에 실패했습니다",
|
||||||
|
"bulkTagsAddFailed": "모델에 태그 추가에 실패했습니다",
|
||||||
|
"bulkTagsReplaceFailed": "모델의 태그 교체에 실패했습니다"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "최소 하나의 검색 옵션을 선택해야 합니다"
|
"atLeastOneOption": "최소 하나의 검색 옵션을 선택해야 합니다"
|
||||||
|
|||||||
@@ -194,7 +194,7 @@
|
|||||||
"displayDensity": "Плотность отображения",
|
"displayDensity": "Плотность отображения",
|
||||||
"displayDensityOptions": {
|
"displayDensityOptions": {
|
||||||
"default": "По умолчанию",
|
"default": "По умолчанию",
|
||||||
"medium": "Средняя",
|
"medium": "Средняя",
|
||||||
"compact": "Компактная"
|
"compact": "Компактная"
|
||||||
},
|
},
|
||||||
"displayDensityHelp": "Выберите количество карточек для отображения в ряду:",
|
"displayDensityHelp": "Выберите количество карточек для отображения в ряду:",
|
||||||
@@ -231,7 +231,7 @@
|
|||||||
"templateOptions": {
|
"templateOptions": {
|
||||||
"flatStructure": "Плоская структура",
|
"flatStructure": "Плоская структура",
|
||||||
"byBaseModel": "По базовой модели",
|
"byBaseModel": "По базовой модели",
|
||||||
"byAuthor": "По автору",
|
"byAuthor": "По автору",
|
||||||
"byFirstTag": "По первому тегу",
|
"byFirstTag": "По первому тегу",
|
||||||
"baseModelFirstTag": "Базовая модель + Первый тег",
|
"baseModelFirstTag": "Базовая модель + Первый тег",
|
||||||
"baseModelAuthor": "Базовая модель + Автор",
|
"baseModelAuthor": "Базовая модель + Автор",
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"customTemplatePlaceholder": "Введите пользовательский шаблон (например, {base_model}/{author}/{first_tag})",
|
"customTemplatePlaceholder": "Введите пользовательский шаблон (например, {base_model}/{author}/{first_tag})",
|
||||||
"modelTypes": {
|
"modelTypes": {
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
"checkpoint": "Checkpoint",
|
"checkpoint": "Checkpoint",
|
||||||
"embedding": "Embedding"
|
"embedding": "Embedding"
|
||||||
},
|
},
|
||||||
"baseModelPathMappings": "Сопоставления путей базовых моделей",
|
"baseModelPathMappings": "Сопоставления путей базовых моделей",
|
||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "Выбрано {count}",
|
"selected": "Выбрано {count}",
|
||||||
"selectedSuffix": "выбрано",
|
"selectedSuffix": "выбрано",
|
||||||
"viewSelected": "Нажмите для просмотра выбранных элементов",
|
"viewSelected": "Нажмите для просмотра выбранных элементов",
|
||||||
|
"addTags": "Добавить теги",
|
||||||
"sendToWorkflow": "Отправить в Workflow",
|
"sendToWorkflow": "Отправить в Workflow",
|
||||||
"copyAll": "Копировать все",
|
"copyAll": "Копировать все",
|
||||||
"refreshAll": "Обновить все",
|
"refreshAll": "Обновить все",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "моделей будут удалены навсегда.",
|
"countMessage": "моделей будут удалены навсегда.",
|
||||||
"action": "Удалить все"
|
"action": "Удалить все"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "Добавить теги к нескольким моделям",
|
||||||
|
"description": "Добавить теги к",
|
||||||
|
"models": "моделям",
|
||||||
|
"tagsToAdd": "Теги для добавления",
|
||||||
|
"placeholder": "Введите тег и нажмите Enter...",
|
||||||
|
"appendTags": "Добавить теги",
|
||||||
|
"replaceTags": "Заменить теги",
|
||||||
|
"saveChanges": "Сохранить изменения"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "Локальные примеры изображений",
|
"title": "Локальные примеры изображений",
|
||||||
"message": "Локальные примеры изображений для этой модели не найдены. Варианты просмотра:",
|
"message": "Локальные примеры изображений для этой модели не найдены. Варианты просмотра:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "Эта группа уже была проверена",
|
"verificationAlreadyDone": "Эта группа уже была проверена",
|
||||||
"verificationCompleteMismatch": "Проверка завершена. {count} файл(ов) имеют разные фактические хеши.",
|
"verificationCompleteMismatch": "Проверка завершена. {count} файл(ов) имеют разные фактические хеши.",
|
||||||
"verificationCompleteSuccess": "Проверка завершена. Все файлы подтверждены как дубликаты.",
|
"verificationCompleteSuccess": "Проверка завершена. Все файлы подтверждены как дубликаты.",
|
||||||
"verificationFailed": "Не удалось проверить хеши: {message}"
|
"verificationFailed": "Не удалось проверить хеши: {message}",
|
||||||
|
"noTagsToAdd": "Нет тегов для добавления",
|
||||||
|
"tagsAddedSuccessfully": "Успешно добавлено {tagCount} тег(ов) к {count} {type}(ам)",
|
||||||
|
"tagsReplacedSuccessfully": "Успешно заменены теги для {count} {type}(ов) на {tagCount} тег(ов)",
|
||||||
|
"tagsAddFailed": "Не удалось добавить теги к {count} модель(ям)",
|
||||||
|
"tagsReplaceFailed": "Не удалось заменить теги для {count} модель(ей)",
|
||||||
|
"bulkTagsAddFailed": "Не удалось добавить теги к моделям",
|
||||||
|
"bulkTagsReplaceFailed": "Не удалось заменить теги для моделей"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "Должен быть выбран хотя бы один вариант поиска"
|
"atLeastOneOption": "Должен быть выбран хотя бы один вариант поиска"
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "已选中 {count} 项",
|
"selected": "已选中 {count} 项",
|
||||||
"selectedSuffix": "已选中",
|
"selectedSuffix": "已选中",
|
||||||
"viewSelected": "点击查看已选项目",
|
"viewSelected": "点击查看已选项目",
|
||||||
|
"addTags": "批量添加标签",
|
||||||
"sendToWorkflow": "发送到工作流",
|
"sendToWorkflow": "发送到工作流",
|
||||||
"copyAll": "全部复制",
|
"copyAll": "全部复制",
|
||||||
"refreshAll": "全部刷新",
|
"refreshAll": "全部刷新",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "模型将被永久删除。",
|
"countMessage": "模型将被永久删除。",
|
||||||
"action": "全部删除"
|
"action": "全部删除"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "批量添加标签",
|
||||||
|
"description": "为多个模型添加标签",
|
||||||
|
"models": "个模型",
|
||||||
|
"tagsToAdd": "要添加的标签",
|
||||||
|
"placeholder": "输入标签并按回车...",
|
||||||
|
"appendTags": "追加标签",
|
||||||
|
"replaceTags": "替换标签",
|
||||||
|
"saveChanges": "保存更改"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "本地示例图片",
|
"title": "本地示例图片",
|
||||||
"message": "未找到此模型的本地示例图片。可选操作:",
|
"message": "未找到此模型的本地示例图片。可选操作:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "此组已验证过",
|
"verificationAlreadyDone": "此组已验证过",
|
||||||
"verificationCompleteMismatch": "验证完成。{count} 个文件实际哈希不同。",
|
"verificationCompleteMismatch": "验证完成。{count} 个文件实际哈希不同。",
|
||||||
"verificationCompleteSuccess": "验证完成。所有文件均为重复项。",
|
"verificationCompleteSuccess": "验证完成。所有文件均为重复项。",
|
||||||
"verificationFailed": "验证哈希失败:{message}"
|
"verificationFailed": "验证哈希失败:{message}",
|
||||||
|
"noTagsToAdd": "没有可添加的标签",
|
||||||
|
"tagsAddedSuccessfully": "已成功为 {count} 个 {type} 添加 {tagCount} 个标签",
|
||||||
|
"tagsReplacedSuccessfully": "已成功为 {count} 个 {type} 替换为 {tagCount} 个标签",
|
||||||
|
"tagsAddFailed": "为 {count} 个模型添加标签失败",
|
||||||
|
"tagsReplaceFailed": "为 {count} 个模型替换标签失败",
|
||||||
|
"bulkTagsAddFailed": "批量添加标签失败",
|
||||||
|
"bulkTagsReplaceFailed": "批量替换标签失败"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "至少选择一个搜索选项"
|
"atLeastOneOption": "至少选择一个搜索选项"
|
||||||
|
|||||||
@@ -319,6 +319,7 @@
|
|||||||
"selected": "已選擇 {count} 項",
|
"selected": "已選擇 {count} 項",
|
||||||
"selectedSuffix": "已選擇",
|
"selectedSuffix": "已選擇",
|
||||||
"viewSelected": "點擊檢視已選項目",
|
"viewSelected": "點擊檢視已選項目",
|
||||||
|
"addTags": "新增標籤",
|
||||||
"sendToWorkflow": "傳送到工作流",
|
"sendToWorkflow": "傳送到工作流",
|
||||||
"copyAll": "全部複製",
|
"copyAll": "全部複製",
|
||||||
"refreshAll": "全部刷新",
|
"refreshAll": "全部刷新",
|
||||||
@@ -572,6 +573,16 @@
|
|||||||
"countMessage": "模型將被永久刪除。",
|
"countMessage": "模型將被永久刪除。",
|
||||||
"action": "全部刪除"
|
"action": "全部刪除"
|
||||||
},
|
},
|
||||||
|
"bulkAddTags": {
|
||||||
|
"title": "新增標籤到多個模型",
|
||||||
|
"description": "新增標籤到",
|
||||||
|
"models": "個模型",
|
||||||
|
"tagsToAdd": "要新增的標籤",
|
||||||
|
"placeholder": "輸入標籤並按 Enter...",
|
||||||
|
"appendTags": "附加標籤",
|
||||||
|
"replaceTags": "取代標籤",
|
||||||
|
"saveChanges": "儲存變更"
|
||||||
|
},
|
||||||
"exampleAccess": {
|
"exampleAccess": {
|
||||||
"title": "本機範例圖片",
|
"title": "本機範例圖片",
|
||||||
"message": "此模型未找到本機範例圖片。可選擇:",
|
"message": "此模型未找到本機範例圖片。可選擇:",
|
||||||
@@ -987,7 +998,14 @@
|
|||||||
"verificationAlreadyDone": "此群組已驗證過",
|
"verificationAlreadyDone": "此群組已驗證過",
|
||||||
"verificationCompleteMismatch": "驗證完成。{count} 個檔案的實際雜湊不同。",
|
"verificationCompleteMismatch": "驗證完成。{count} 個檔案的實際雜湊不同。",
|
||||||
"verificationCompleteSuccess": "驗證完成。所有檔案均確認為重複項。",
|
"verificationCompleteSuccess": "驗證完成。所有檔案均確認為重複項。",
|
||||||
"verificationFailed": "驗證雜湊失敗:{message}"
|
"verificationFailed": "驗證雜湊失敗:{message}",
|
||||||
|
"noTagsToAdd": "沒有可新增的標籤",
|
||||||
|
"tagsAddedSuccessfully": "已成功將 {tagCount} 個標籤新增到 {count} 個 {type}",
|
||||||
|
"tagsReplacedSuccessfully": "已成功以 {tagCount} 個標籤取代 {count} 個 {type} 的標籤",
|
||||||
|
"tagsAddFailed": "新增標籤到 {count} 個模型失敗",
|
||||||
|
"tagsReplaceFailed": "取代 {count} 個模型的標籤失敗",
|
||||||
|
"bulkTagsAddFailed": "批量新增標籤到模型失敗",
|
||||||
|
"bulkTagsReplaceFailed": "批量取代模型標籤失敗"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"atLeastOneOption": "至少需選擇一個搜尋選項"
|
"atLeastOneOption": "至少需選擇一個搜尋選項"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class BaseModelRoutes(ABC):
|
|||||||
app.router.add_post(f'/api/{prefix}/relink-civitai', self.relink_civitai)
|
app.router.add_post(f'/api/{prefix}/relink-civitai', self.relink_civitai)
|
||||||
app.router.add_post(f'/api/{prefix}/replace-preview', self.replace_preview)
|
app.router.add_post(f'/api/{prefix}/replace-preview', self.replace_preview)
|
||||||
app.router.add_post(f'/api/{prefix}/save-metadata', self.save_metadata)
|
app.router.add_post(f'/api/{prefix}/save-metadata', self.save_metadata)
|
||||||
|
app.router.add_post(f'/api/{prefix}/add-tags', self.add_tags)
|
||||||
app.router.add_post(f'/api/{prefix}/rename', self.rename_model)
|
app.router.add_post(f'/api/{prefix}/rename', self.rename_model)
|
||||||
app.router.add_post(f'/api/{prefix}/bulk-delete', self.bulk_delete_models)
|
app.router.add_post(f'/api/{prefix}/bulk-delete', self.bulk_delete_models)
|
||||||
app.router.add_post(f'/api/{prefix}/verify-duplicates', self.verify_duplicates)
|
app.router.add_post(f'/api/{prefix}/verify-duplicates', self.verify_duplicates)
|
||||||
@@ -272,6 +273,10 @@ class BaseModelRoutes(ABC):
|
|||||||
"""Handle saving metadata updates"""
|
"""Handle saving metadata updates"""
|
||||||
return await ModelRouteUtils.handle_save_metadata(request, self.service.scanner)
|
return await ModelRouteUtils.handle_save_metadata(request, self.service.scanner)
|
||||||
|
|
||||||
|
async def add_tags(self, request: web.Request) -> web.Response:
|
||||||
|
"""Handle adding tags to model metadata"""
|
||||||
|
return await ModelRouteUtils.handle_add_tags(request, self.service.scanner)
|
||||||
|
|
||||||
async def rename_model(self, request: web.Request) -> web.Response:
|
async def rename_model(self, request: web.Request) -> web.Response:
|
||||||
"""Handle renaming a model file and its associated files"""
|
"""Handle renaming a model file and its associated files"""
|
||||||
return await ModelRouteUtils.handle_rename_model(request, self.service.scanner)
|
return await ModelRouteUtils.handle_rename_model(request, self.service.scanner)
|
||||||
|
|||||||
@@ -870,11 +870,11 @@ class ModelRouteUtils:
|
|||||||
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
||||||
|
|
||||||
# Compare hashes
|
# Compare hashes
|
||||||
stored_hash = metadata.get('sha256', '').lower()
|
stored_hash = metadata.get('sha256', '').lower();
|
||||||
|
|
||||||
# Set expected hash from first file if not yet set
|
# Set expected hash from first file if not yet set
|
||||||
if not expected_hash:
|
if not expected_hash:
|
||||||
expected_hash = stored_hash
|
expected_hash = stored_hash;
|
||||||
|
|
||||||
# Check if hash matches expected hash
|
# Check if hash matches expected hash
|
||||||
if actual_hash != expected_hash:
|
if actual_hash != expected_hash:
|
||||||
@@ -978,7 +978,7 @@ class ModelRouteUtils:
|
|||||||
if os.path.exists(metadata_path):
|
if os.path.exists(metadata_path):
|
||||||
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
||||||
hash_value = metadata.get('sha256')
|
hash_value = metadata.get('sha256')
|
||||||
|
logger.info(f"hash_value: {hash_value}, metadata_path: {metadata_path}, metadata: {metadata}")
|
||||||
# Rename all files
|
# Rename all files
|
||||||
renamed_files = []
|
renamed_files = []
|
||||||
new_metadata_path = None
|
new_metadata_path = None
|
||||||
@@ -1093,3 +1093,63 @@ class ModelRouteUtils:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving metadata: {e}", exc_info=True)
|
logger.error(f"Error saving metadata: {e}", exc_info=True)
|
||||||
return web.Response(text=str(e), status=500)
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def handle_add_tags(request: web.Request, scanner) -> web.Response:
|
||||||
|
"""Handle adding tags to model metadata
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The aiohttp request
|
||||||
|
scanner: The model scanner instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
web.Response: The HTTP response
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
file_path = data.get('file_path')
|
||||||
|
new_tags = data.get('tags', [])
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return web.Response(text='File path is required', status=400)
|
||||||
|
|
||||||
|
if not isinstance(new_tags, list):
|
||||||
|
return web.Response(text='Tags must be a list', status=400)
|
||||||
|
|
||||||
|
# Get metadata file path
|
||||||
|
metadata_path = os.path.splitext(file_path)[0] + '.metadata.json'
|
||||||
|
|
||||||
|
# Load existing metadata
|
||||||
|
metadata = await ModelRouteUtils.load_local_metadata(metadata_path)
|
||||||
|
|
||||||
|
# Get existing tags (case insensitive)
|
||||||
|
existing_tags = metadata.get('tags', [])
|
||||||
|
existing_tags_lower = [tag.lower() for tag in existing_tags]
|
||||||
|
|
||||||
|
# Add new tags that don't already exist (case insensitive check)
|
||||||
|
tags_added = []
|
||||||
|
for tag in new_tags:
|
||||||
|
if isinstance(tag, str) and tag.strip():
|
||||||
|
tag_stripped = tag.strip()
|
||||||
|
if tag_stripped.lower() not in existing_tags_lower:
|
||||||
|
existing_tags.append(tag_stripped)
|
||||||
|
existing_tags_lower.append(tag_stripped.lower())
|
||||||
|
tags_added.append(tag_stripped)
|
||||||
|
|
||||||
|
# Update metadata with combined tags
|
||||||
|
metadata['tags'] = existing_tags
|
||||||
|
|
||||||
|
# Save updated metadata
|
||||||
|
await MetadataManager.save_metadata(file_path, metadata)
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
await scanner.update_single_model_cache(file_path, file_path, metadata)
|
||||||
|
|
||||||
|
return web.json_response({
|
||||||
|
'success': True,
|
||||||
|
'tags': existing_tags
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding tags: {e}", exc_info=True)
|
||||||
|
return web.Response(text=str(e), status=500)
|
||||||
|
|||||||
293
scripts/sync_translation_keys.py
Normal file
293
scripts/sync_translation_keys.py
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Translation Key Synchronization Script
|
||||||
|
|
||||||
|
This script synchronizes new translation keys from en.json to all other locale files
|
||||||
|
while maintaining exact formatting consistency to pass test_i18n.py validation.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Preserves exact line-by-line formatting
|
||||||
|
- Maintains proper indentation and structure
|
||||||
|
- Adds missing keys with placeholder translations
|
||||||
|
- Handles nested objects correctly
|
||||||
|
- Ensures all locale files have identical structure
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/sync_translation_keys.py [--dry-run] [--verbose]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
|
from typing import Dict, List, Set, Tuple, Any, Optional
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
# Add the parent directory to the path so we can import modules if needed
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
class TranslationKeySynchronizer:
|
||||||
|
"""Synchronizes translation keys across locale files while maintaining formatting."""
|
||||||
|
|
||||||
|
def __init__(self, locales_dir: str, verbose: bool = False):
|
||||||
|
self.locales_dir = locales_dir
|
||||||
|
self.verbose = verbose
|
||||||
|
self.reference_locale = 'en'
|
||||||
|
self.target_locales = ['zh-CN', 'zh-TW', 'ja', 'ru', 'de', 'fr', 'es', 'ko']
|
||||||
|
|
||||||
|
def log(self, message: str, level: str = 'INFO'):
|
||||||
|
"""Log a message if verbose mode is enabled."""
|
||||||
|
if self.verbose or level == 'ERROR':
|
||||||
|
print(f"[{level}] {message}")
|
||||||
|
|
||||||
|
def load_json_preserve_order(self, file_path: str) -> Tuple[Dict[str, Any], List[str]]:
|
||||||
|
"""
|
||||||
|
Load a JSON file preserving the exact order and formatting.
|
||||||
|
Returns both the parsed data and the original lines.
|
||||||
|
"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
content = ''.join(lines)
|
||||||
|
|
||||||
|
# Parse JSON while preserving order
|
||||||
|
data = json.loads(content, object_pairs_hook=OrderedDict)
|
||||||
|
return data, lines
|
||||||
|
|
||||||
|
def get_all_leaf_keys(self, data: Any, prefix: str = '') -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract all leaf keys (non-object values) with their full paths.
|
||||||
|
Returns a dictionary mapping full key paths to their values.
|
||||||
|
"""
|
||||||
|
keys = {}
|
||||||
|
|
||||||
|
if isinstance(data, (dict, OrderedDict)):
|
||||||
|
for key, value in data.items():
|
||||||
|
full_key = f"{prefix}.{key}" if prefix else key
|
||||||
|
|
||||||
|
if isinstance(value, (dict, OrderedDict)):
|
||||||
|
# Recursively get nested keys
|
||||||
|
keys.update(self.get_all_leaf_keys(value, full_key))
|
||||||
|
else:
|
||||||
|
# Leaf node - actual translatable value
|
||||||
|
keys[full_key] = value
|
||||||
|
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def merge_json_structures(self, reference_data: Dict[str, Any], target_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Merge the reference JSON structure with existing target translations.
|
||||||
|
This creates a new structure that matches the reference exactly but preserves
|
||||||
|
existing translations where available.
|
||||||
|
"""
|
||||||
|
def merge_recursive(ref_obj, target_obj):
|
||||||
|
if isinstance(ref_obj, (dict, OrderedDict)):
|
||||||
|
result = OrderedDict()
|
||||||
|
for key, ref_value in ref_obj.items():
|
||||||
|
if key in target_obj and isinstance(target_obj[key], type(ref_value)):
|
||||||
|
# Key exists in target with same type
|
||||||
|
if isinstance(ref_value, (dict, OrderedDict)):
|
||||||
|
# Recursively merge nested objects
|
||||||
|
result[key] = merge_recursive(ref_value, target_obj[key])
|
||||||
|
else:
|
||||||
|
# Use existing translation
|
||||||
|
result[key] = target_obj[key]
|
||||||
|
else:
|
||||||
|
# Key missing in target or type mismatch
|
||||||
|
if isinstance(ref_value, (dict, OrderedDict)):
|
||||||
|
# Recursively handle nested objects
|
||||||
|
result[key] = merge_recursive(ref_value, {})
|
||||||
|
else:
|
||||||
|
# Create placeholder translation
|
||||||
|
result[key] = f"[TODO: Translate] {ref_value}"
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# For non-dict values, use reference (this shouldn't happen at root level)
|
||||||
|
return ref_obj
|
||||||
|
|
||||||
|
return merge_recursive(reference_data, target_data)
|
||||||
|
|
||||||
|
def format_json_like_reference(self, data: Dict[str, Any], reference_lines: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Format the merged JSON data to match the reference file's formatting exactly.
|
||||||
|
"""
|
||||||
|
# Use json.dumps with proper formatting to match the reference style
|
||||||
|
formatted_json = json.dumps(data, indent=4, ensure_ascii=False, separators=(',', ': '))
|
||||||
|
|
||||||
|
# Split into lines and ensure consistent line endings
|
||||||
|
formatted_lines = [line + '\n' for line in formatted_json.split('\n')]
|
||||||
|
|
||||||
|
# Make sure the last line doesn't have extra newlines
|
||||||
|
if formatted_lines and formatted_lines[-1].strip() == '':
|
||||||
|
formatted_lines = formatted_lines[:-1]
|
||||||
|
|
||||||
|
# Ensure the last line ends with just a newline
|
||||||
|
if formatted_lines and not formatted_lines[-1].endswith('\n'):
|
||||||
|
formatted_lines[-1] += '\n'
|
||||||
|
|
||||||
|
return formatted_lines
|
||||||
|
|
||||||
|
def synchronize_locale_simple(self, locale: str, reference_data: Dict[str, Any],
|
||||||
|
reference_lines: List[str], dry_run: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Synchronize a locale file using JSON structure merging.
|
||||||
|
"""
|
||||||
|
locale_file = os.path.join(self.locales_dir, f'{locale}.json')
|
||||||
|
|
||||||
|
if not os.path.exists(locale_file):
|
||||||
|
self.log(f"Locale file {locale_file} does not exist!", 'ERROR')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_data, _ = self.load_json_preserve_order(locale_file)
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error loading {locale_file}: {e}", 'ERROR')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get keys to check for missing ones
|
||||||
|
ref_keys = self.get_all_leaf_keys(reference_data)
|
||||||
|
target_keys = self.get_all_leaf_keys(target_data)
|
||||||
|
missing_keys = set(ref_keys.keys()) - set(target_keys.keys())
|
||||||
|
|
||||||
|
if not missing_keys:
|
||||||
|
self.log(f"Locale {locale} is already up to date")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log(f"Found {len(missing_keys)} missing keys in {locale}:")
|
||||||
|
for key in sorted(missing_keys):
|
||||||
|
self.log(f" - {key}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.log(f"DRY RUN: Would update {locale} with {len(missing_keys)} new keys")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Merge the structures
|
||||||
|
try:
|
||||||
|
merged_data = self.merge_json_structures(reference_data, target_data)
|
||||||
|
|
||||||
|
# Format to match reference style
|
||||||
|
new_lines = self.format_json_like_reference(merged_data, reference_lines)
|
||||||
|
|
||||||
|
# Validate that the result is valid JSON
|
||||||
|
reconstructed_content = ''.join(new_lines)
|
||||||
|
json.loads(reconstructed_content) # This will raise an exception if invalid
|
||||||
|
|
||||||
|
# Write the updated file
|
||||||
|
with open(locale_file, 'w', encoding='utf-8') as f:
|
||||||
|
f.writelines(new_lines)
|
||||||
|
|
||||||
|
self.log(f"Successfully updated {locale} with {len(missing_keys)} new keys")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.log(f"Generated invalid JSON for {locale}: {e}", 'ERROR')
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error updating {locale_file}: {e}", 'ERROR')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def synchronize_all(self, dry_run: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Synchronize all locale files with the reference.
|
||||||
|
Returns True if all operations were successful.
|
||||||
|
"""
|
||||||
|
# Load reference file
|
||||||
|
reference_file = os.path.join(self.locales_dir, f'{self.reference_locale}.json')
|
||||||
|
|
||||||
|
if not os.path.exists(reference_file):
|
||||||
|
self.log(f"Reference file {reference_file} does not exist!", 'ERROR')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
reference_data, reference_lines = self.load_json_preserve_order(reference_file)
|
||||||
|
reference_keys = self.get_all_leaf_keys(reference_data)
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error loading reference file: {e}", 'ERROR')
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.log(f"Loaded reference file with {len(reference_keys)} keys")
|
||||||
|
|
||||||
|
success = True
|
||||||
|
changes_made = False
|
||||||
|
|
||||||
|
# Synchronize each target locale
|
||||||
|
for locale in self.target_locales:
|
||||||
|
try:
|
||||||
|
if self.synchronize_locale_simple(locale, reference_data, reference_lines, dry_run):
|
||||||
|
changes_made = True
|
||||||
|
except Exception as e:
|
||||||
|
self.log(f"Error synchronizing {locale}: {e}", 'ERROR')
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if changes_made:
|
||||||
|
self.log("Synchronization completed with changes")
|
||||||
|
else:
|
||||||
|
self.log("All locale files are already up to date")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for the script."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Synchronize translation keys from en.json to all other locale files'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='Show what would be changed without making actual changes'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--verbose', '-v',
|
||||||
|
action='store_true',
|
||||||
|
help='Enable verbose output'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--locales-dir',
|
||||||
|
default=None,
|
||||||
|
help='Path to locales directory (default: auto-detect from script location)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Determine locales directory
|
||||||
|
if args.locales_dir:
|
||||||
|
locales_dir = args.locales_dir
|
||||||
|
else:
|
||||||
|
# Auto-detect based on script location
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
locales_dir = os.path.join(os.path.dirname(script_dir), 'locales')
|
||||||
|
|
||||||
|
if not os.path.exists(locales_dir):
|
||||||
|
print(f"ERROR: Locales directory not found: {locales_dir}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"Translation Key Synchronization")
|
||||||
|
print(f"Locales directory: {locales_dir}")
|
||||||
|
print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE UPDATE'}")
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
# Create synchronizer and run
|
||||||
|
synchronizer = TranslationKeySynchronizer(locales_dir, args.verbose)
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = synchronizer.synchronize_all(args.dry_run)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n✅ Synchronization completed successfully!")
|
||||||
|
if not args.dry_run:
|
||||||
|
print("💡 Run 'python test_i18n.py' to verify formatting consistency")
|
||||||
|
else:
|
||||||
|
print("\n❌ Synchronization completed with errors!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⚠️ Operation cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Unexpected error: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -176,11 +176,6 @@
|
|||||||
background: linear-gradient(45deg, #4a90e2, #357abd);
|
background: linear-gradient(45deg, #4a90e2, #357abd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove old node-color-indicator styles */
|
|
||||||
.node-color-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-all-item {
|
.send-all-item {
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -217,4 +212,22 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bulk Context Menu Header */
|
||||||
|
.bulk-context-header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: var(--border-radius-xs) var(--border-radius-xs) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-context-header i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,10 @@ body.modal-open {
|
|||||||
overflow-x: hidden; /* 防止水平滚动条 */
|
overflow-x: hidden; /* 防止水平滚动条 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content-large {
|
||||||
|
min-height: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 当 modal 打开时锁定 body */
|
/* 当 modal 打开时锁定 body */
|
||||||
body.modal-open {
|
body.modal-open {
|
||||||
overflow: hidden !important; /* 覆盖 base.css 中的 scroll */
|
overflow: hidden !important; /* 覆盖 base.css 中的 scroll */
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 30px; /* Ensure some height even if empty to prevent layout shifts */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual Item */
|
/* Individual Item */
|
||||||
@@ -153,17 +154,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metadata-save-btn,
|
.metadata-save-btn,
|
||||||
.save-tags-btn {
|
.save-tags-btn,
|
||||||
|
.append-tags-btn,
|
||||||
|
.replace-tags-btn {
|
||||||
background: var(--lora-accent) !important;
|
background: var(--lora-accent) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
border-color: var(--lora-accent) !important;
|
border-color: var(--lora-accent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-save-btn:hover,
|
.metadata-save-btn:hover,
|
||||||
.save-tags-btn:hover {
|
.save-tags-btn:hover,
|
||||||
|
.append-tags-btn:hover,
|
||||||
|
.replace-tags-btn:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Specific styling for bulk tag action buttons */
|
||||||
|
.bulk-append-tags-btn {
|
||||||
|
background: var(--lora-accent) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-color: var(--lora-accent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-replace-tags-btn {
|
||||||
|
background: var(--lora-warning, #f59e0b) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-color: var(--lora-warning, #f59e0b) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-append-tags-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-replace-tags-btn:hover {
|
||||||
|
background: var(--lora-warning-dark, #d97706) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Add Form */
|
/* Add Form */
|
||||||
.metadata-add-form {
|
.metadata-add-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export function getApiEndpoints(modelType) {
|
|||||||
|
|
||||||
// Bulk operations
|
// Bulk operations
|
||||||
bulkDelete: `/api/${modelType}/bulk-delete`,
|
bulkDelete: `/api/${modelType}/bulk-delete`,
|
||||||
|
|
||||||
|
// Tag operations
|
||||||
|
addTags: `/api/${modelType}/add-tags`,
|
||||||
|
|
||||||
// Move operations (now common for all model types that support move)
|
// Move operations (now common for all model types that support move)
|
||||||
moveModel: `/api/${modelType}/move_model`,
|
moveModel: `/api/${modelType}/move_model`,
|
||||||
|
|||||||
@@ -306,6 +306,34 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addTags(filePath, data) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.addTags, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: filePath,
|
||||||
|
...data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to add tags');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success && result.tags) {
|
||||||
|
state.virtualScroller.updateSingleItem(filePath, { tags: result.tags });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding tags:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async refreshModels(fullRebuild = false) {
|
async refreshModels(fullRebuild = false) {
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading(
|
state.loadingManager.showSimpleLoading(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
import { createPageControls } from './components/controls/index.js';
|
import { createPageControls } from './components/controls/index.js';
|
||||||
import { CheckpointContextMenu } from './components/ContextMenu/index.js';
|
|
||||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||||
import { MODEL_TYPES } from './api/apiConfig.js';
|
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||||
|
|
||||||
@@ -30,10 +29,7 @@ class CheckpointsPageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Initialize context menu
|
// Initialize common page features (including context menus)
|
||||||
new CheckpointContextMenu();
|
|
||||||
|
|
||||||
// Initialize common page features
|
|
||||||
appCore.initializePageFeatures();
|
appCore.initializePageFeatures();
|
||||||
|
|
||||||
console.log('Checkpoints Manager initialized');
|
console.log('Checkpoints Manager initialized');
|
||||||
@@ -48,4 +44,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Initialize checkpoints page
|
// Initialize checkpoints page
|
||||||
const checkpointsPage = new CheckpointsPageManager();
|
const checkpointsPage = new CheckpointsPageManager();
|
||||||
await checkpointsPage.initialize();
|
await checkpointsPage.initialize();
|
||||||
});
|
});
|
||||||
@@ -15,17 +15,6 @@ export class BaseContextMenu {
|
|||||||
init() {
|
init() {
|
||||||
// Hide menu on regular clicks
|
// Hide menu on regular clicks
|
||||||
document.addEventListener('click', () => this.hideMenu());
|
document.addEventListener('click', () => this.hideMenu());
|
||||||
|
|
||||||
// Show menu on right-click on cards
|
|
||||||
document.addEventListener('contextmenu', (e) => {
|
|
||||||
const card = e.target.closest(this.cardSelector);
|
|
||||||
if (!card) {
|
|
||||||
this.hideMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
this.showMenu(e.clientX, e.clientY, card);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle menu item clicks
|
// Handle menu item clicks
|
||||||
this.menu.addEventListener('click', (e) => {
|
this.menu.addEventListener('click', (e) => {
|
||||||
|
|||||||
96
static/js/components/ContextMenu/BulkContextMenu.js
Normal file
96
static/js/components/ContextMenu/BulkContextMenu.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
import { bulkManager } from '../../managers/BulkManager.js';
|
||||||
|
import { updateElementText } from '../../utils/i18nHelpers.js';
|
||||||
|
|
||||||
|
export class BulkContextMenu extends BaseContextMenu {
|
||||||
|
constructor() {
|
||||||
|
super('bulkContextMenu', '.model-card.selected');
|
||||||
|
this.setupBulkMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBulkMenuItems() {
|
||||||
|
if (!this.menu) return;
|
||||||
|
|
||||||
|
// Update menu items visibility based on current model type
|
||||||
|
this.updateMenuItemsForModelType();
|
||||||
|
|
||||||
|
// Update selected count in header
|
||||||
|
this.updateSelectedCountHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenuItemsForModelType() {
|
||||||
|
const currentModelType = state.currentPageType;
|
||||||
|
const config = bulkManager.actionConfig[currentModelType];
|
||||||
|
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
// Update button visibility based on model type
|
||||||
|
const addTagsItem = this.menu.querySelector('[data-action="add-tags"]');
|
||||||
|
const sendToWorkflowItem = this.menu.querySelector('[data-action="send-to-workflow"]');
|
||||||
|
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||||
|
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||||
|
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||||
|
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||||
|
|
||||||
|
if (sendToWorkflowItem) {
|
||||||
|
sendToWorkflowItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (copyAllItem) {
|
||||||
|
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (refreshAllItem) {
|
||||||
|
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (moveAllItem) {
|
||||||
|
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (deleteAllItem) {
|
||||||
|
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (addTagsItem) {
|
||||||
|
addTagsItem.style.display = config.addTags ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedCountHeader() {
|
||||||
|
const headerElement = this.menu.querySelector('.bulk-context-header');
|
||||||
|
if (headerElement) {
|
||||||
|
updateElementText(headerElement, 'loras.bulkOperations.selected', { count: state.selectedModels.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenu(x, y, card) {
|
||||||
|
this.updateMenuItemsForModelType();
|
||||||
|
this.updateSelectedCountHeader();
|
||||||
|
super.showMenu(x, y, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMenuAction(action, menuItem) {
|
||||||
|
switch (action) {
|
||||||
|
case 'add-tags':
|
||||||
|
bulkManager.showBulkAddTagsModal();
|
||||||
|
break;
|
||||||
|
case 'send-to-workflow':
|
||||||
|
bulkManager.sendAllModelsToWorkflow();
|
||||||
|
break;
|
||||||
|
case 'copy-all':
|
||||||
|
bulkManager.copyAllModelsSyntax();
|
||||||
|
break;
|
||||||
|
case 'refresh-all':
|
||||||
|
bulkManager.refreshAllMetadata();
|
||||||
|
break;
|
||||||
|
case 'move-all':
|
||||||
|
window.moveManager.showMoveModal('bulk');
|
||||||
|
break;
|
||||||
|
case 'delete-all':
|
||||||
|
bulkManager.showBulkDeleteModal();
|
||||||
|
break;
|
||||||
|
case 'clear':
|
||||||
|
bulkManager.clearSelection();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown bulk action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,56 @@ export { LoraContextMenu } from './LoraContextMenu.js';
|
|||||||
export { RecipeContextMenu } from './RecipeContextMenu.js';
|
export { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||||
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||||
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||||
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||||
|
|
||||||
|
import { LoraContextMenu } from './LoraContextMenu.js';
|
||||||
|
import { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||||
|
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||||
|
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
|
||||||
|
// Factory method to create page-specific context menu instances
|
||||||
|
export function createPageContextMenu(pageType) {
|
||||||
|
switch (pageType) {
|
||||||
|
case 'loras':
|
||||||
|
return new LoraContextMenu();
|
||||||
|
case 'recipes':
|
||||||
|
return new RecipeContextMenu();
|
||||||
|
case 'checkpoints':
|
||||||
|
return new CheckpointContextMenu();
|
||||||
|
case 'embeddings':
|
||||||
|
return new EmbeddingContextMenu();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize context menu coordination for pages that support it
|
||||||
|
export function initializeContextMenuCoordination(pageContextMenu, bulkContextMenu) {
|
||||||
|
// Centralized context menu event handler
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
const card = e.target.closest('.model-card');
|
||||||
|
if (!card) {
|
||||||
|
// Hide all menus if not right-clicking on a card
|
||||||
|
pageContextMenu?.hideMenu();
|
||||||
|
bulkContextMenu?.hideMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Hide all menus first
|
||||||
|
pageContextMenu?.hideMenu();
|
||||||
|
bulkContextMenu?.hideMenu();
|
||||||
|
|
||||||
|
// Determine which menu to show based on bulk mode and selection state
|
||||||
|
if (state.bulkMode && card.classList.contains('selected')) {
|
||||||
|
// Show bulk menu for selected cards in bulk mode
|
||||||
|
bulkContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||||
|
} else if (!state.bulkMode) {
|
||||||
|
// Show regular menu when not in bulk mode
|
||||||
|
pageContextMenu?.showMenu(e.clientX, e.clientY, card);
|
||||||
|
}
|
||||||
|
// Don't show any menu for unselected cards in bulk mode
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,14 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { PRESET_TAGS } from '../../utils/constants.js';
|
||||||
|
|
||||||
// Preset tag suggestions
|
|
||||||
const PRESET_TAGS = [
|
|
||||||
'character', 'style', 'concept', 'clothing',
|
|
||||||
'poses', 'background', 'vehicle', 'buildings',
|
|
||||||
'objects', 'animal'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create a named function so we can remove it later
|
// Create a named function so we can remove it later
|
||||||
let saveTagsHandler = null;
|
let saveTagsHandler = null;
|
||||||
@@ -139,7 +132,7 @@ export function setupTagEditMode() {
|
|||||||
// ...existing helper functions...
|
// ...existing helper functions...
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save tags - 支持LoRA和Checkpoint
|
* Save tags
|
||||||
*/
|
*/
|
||||||
async function saveTags() {
|
async function saveTags() {
|
||||||
const editBtn = document.querySelector('.edit-tags-btn');
|
const editBtn = document.querySelector('.edit-tags-btn');
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
|||||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||||
import { i18n } from './i18n/index.js';
|
import { i18n } from './i18n/index.js';
|
||||||
import { onboardingManager } from './managers/OnboardingManager.js';
|
import { onboardingManager } from './managers/OnboardingManager.js';
|
||||||
|
import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js';
|
||||||
|
import { createPageContextMenu, initializeContextMenuCoordination } from './components/ContextMenu/index.js';
|
||||||
|
|
||||||
// Core application class
|
// Core application class
|
||||||
export class AppCore {
|
export class AppCore {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
|
this.pageContextMenu = null;
|
||||||
|
this.bulkContextMenu = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize core functionality
|
// Initialize core functionality
|
||||||
@@ -55,6 +59,10 @@ export class AppCore {
|
|||||||
// Initialize the bulk manager
|
// Initialize the bulk manager
|
||||||
bulkManager.initialize();
|
bulkManager.initialize();
|
||||||
|
|
||||||
|
// Initialize bulk context menu
|
||||||
|
const bulkContextMenu = new BulkContextMenu();
|
||||||
|
bulkManager.setBulkContextMenu(bulkContextMenu);
|
||||||
|
|
||||||
// Initialize the example images manager
|
// Initialize the example images manager
|
||||||
exampleImagesManager.initialize();
|
exampleImagesManager.initialize();
|
||||||
// Initialize the help manager
|
// Initialize the help manager
|
||||||
@@ -88,13 +96,27 @@ export class AppCore {
|
|||||||
initializePageFeatures() {
|
initializePageFeatures() {
|
||||||
const pageType = this.getPageType();
|
const pageType = this.getPageType();
|
||||||
|
|
||||||
// Initialize virtual scroll for pages that need it
|
|
||||||
if (['loras', 'recipes', 'checkpoints', 'embeddings'].includes(pageType)) {
|
if (['loras', 'recipes', 'checkpoints', 'embeddings'].includes(pageType)) {
|
||||||
|
this.initializeContextMenus(pageType);
|
||||||
initializeInfiniteScroll(pageType);
|
initializeInfiniteScroll(pageType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize context menus for the current page
|
||||||
|
initializeContextMenus(pageType) {
|
||||||
|
// Create page-specific context menu
|
||||||
|
this.pageContextMenu = createPageContextMenu(pageType);
|
||||||
|
|
||||||
|
// Get bulk context menu from bulkManager
|
||||||
|
this.bulkContextMenu = bulkManager.bulkContextMenu;
|
||||||
|
|
||||||
|
// Initialize context menu coordination
|
||||||
|
if (this.pageContextMenu || this.bulkContextMenu) {
|
||||||
|
initializeContextMenuCoordination(this.pageContextMenu, this.bulkContextMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
import { createPageControls } from './components/controls/index.js';
|
import { createPageControls } from './components/controls/index.js';
|
||||||
import { EmbeddingContextMenu } from './components/ContextMenu/index.js';
|
|
||||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||||
import { MODEL_TYPES } from './api/apiConfig.js';
|
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||||
|
|
||||||
@@ -30,10 +29,7 @@ class EmbeddingsPageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Initialize context menu
|
// Initialize common page features (including context menus)
|
||||||
new EmbeddingContextMenu();
|
|
||||||
|
|
||||||
// Initialize common page features
|
|
||||||
appCore.initializePageFeatures();
|
appCore.initializePageFeatures();
|
||||||
|
|
||||||
console.log('Embeddings Manager initialized');
|
console.log('Embeddings Manager initialized');
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { state } from './state/index.js';
|
import { state } from './state/index.js';
|
||||||
import { updateCardsForBulkMode } from './components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from './components/shared/ModelCard.js';
|
||||||
import { LoraContextMenu } from './components/ContextMenu/index.js';
|
|
||||||
import { createPageControls } from './components/controls/index.js';
|
import { createPageControls } from './components/controls/index.js';
|
||||||
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||||
@@ -37,13 +36,10 @@ class LoraPageManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Initialize page-specific components
|
|
||||||
new LoraContextMenu();
|
|
||||||
|
|
||||||
// Initialize cards for current bulk mode state (should be false initially)
|
// Initialize cards for current bulk mode state (should be false initially)
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
// Initialize common page features (virtual scroll)
|
// Initialize common page features (including context menus and virtual scroll)
|
||||||
appCore.initializePageFeatures();
|
appCore.initializePageFeatures();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,20 @@ import { state, getCurrentPageState } from '../state/index.js';
|
|||||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||||
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 { moveManager } from './MoveManager.js';
|
|
||||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||||
import { updateElementText } from '../utils/i18nHelpers.js';
|
import { PRESET_TAGS } from '../utils/constants.js';
|
||||||
|
|
||||||
export class BulkManager {
|
export class BulkManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
||||||
this.bulkPanel = document.getElementById('bulkOperationsPanel');
|
// Remove bulk panel references since we're using context menu now
|
||||||
this.isStripVisible = false;
|
this.bulkContextMenu = null; // Will be set by core initialization
|
||||||
|
|
||||||
this.stripMaxThumbnails = 50;
|
|
||||||
|
|
||||||
// Model type specific action configurations
|
// Model type specific action configurations
|
||||||
this.actionConfig = {
|
this.actionConfig = {
|
||||||
[MODEL_TYPES.LORA]: {
|
[MODEL_TYPES.LORA]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: true,
|
sendToWorkflow: true,
|
||||||
copyAll: true,
|
copyAll: true,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -25,6 +23,7 @@ export class BulkManager {
|
|||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -32,6 +31,7 @@ export class BulkManager {
|
|||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.CHECKPOINT]: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
@@ -46,41 +46,13 @@ export class BulkManager {
|
|||||||
this.setupGlobalKeyboardListeners();
|
this.setupGlobalKeyboardListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBulkContextMenu(bulkContextMenu) {
|
||||||
|
this.bulkContextMenu = bulkContextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Bulk operations button listeners
|
// Only setup bulk mode toggle button listener now
|
||||||
const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]');
|
// Context menu actions are handled by BulkContextMenu
|
||||||
const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]');
|
|
||||||
const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]');
|
|
||||||
const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]');
|
|
||||||
const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]');
|
|
||||||
const clearBtn = this.bulkPanel?.querySelector('[data-action="clear"]');
|
|
||||||
|
|
||||||
if (sendToWorkflowBtn) {
|
|
||||||
sendToWorkflowBtn.addEventListener('click', () => this.sendAllModelsToWorkflow());
|
|
||||||
}
|
|
||||||
if (copyAllBtn) {
|
|
||||||
copyAllBtn.addEventListener('click', () => this.copyAllModelsSyntax());
|
|
||||||
}
|
|
||||||
if (refreshAllBtn) {
|
|
||||||
refreshAllBtn.addEventListener('click', () => this.refreshAllMetadata());
|
|
||||||
}
|
|
||||||
if (moveAllBtn) {
|
|
||||||
moveAllBtn.addEventListener('click', () => {
|
|
||||||
moveManager.showMoveModal('bulk');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (deleteAllBtn) {
|
|
||||||
deleteAllBtn.addEventListener('click', () => this.showBulkDeleteModal());
|
|
||||||
}
|
|
||||||
if (clearBtn) {
|
|
||||||
clearBtn.addEventListener('click', () => this.clearSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected count click listener
|
|
||||||
const selectedCount = document.getElementById('selectedCount');
|
|
||||||
if (selectedCount) {
|
|
||||||
selectedCount.addEventListener('click', () => this.toggleThumbnailStrip());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGlobalKeyboardListeners() {
|
setupGlobalKeyboardListeners() {
|
||||||
@@ -115,60 +87,15 @@ export class BulkManager {
|
|||||||
|
|
||||||
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
||||||
|
|
||||||
if (state.bulkMode) {
|
|
||||||
this.bulkPanel.classList.remove('hidden');
|
|
||||||
this.updateActionButtonsVisibility();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.bulkPanel.classList.add('visible');
|
|
||||||
}, 10);
|
|
||||||
} else {
|
|
||||||
this.bulkPanel.classList.remove('visible');
|
|
||||||
setTimeout(() => {
|
|
||||||
this.bulkPanel.classList.add('hidden');
|
|
||||||
}, 400);
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
if (!state.bulkMode) {
|
if (!state.bulkMode) {
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
|
|
||||||
// TODO:
|
// Hide context menu when exiting bulk mode
|
||||||
document.querySelectorAll('.model-card').forEach(card => {
|
if (this.bulkContextMenu) {
|
||||||
const actions = card.querySelectorAll('.card-actions, .card-button');
|
this.bulkContextMenu.hideMenu();
|
||||||
actions.forEach(action => action.style.display = 'flex');
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionButtonsVisibility() {
|
|
||||||
const currentModelType = state.currentPageType;
|
|
||||||
const config = this.actionConfig[currentModelType];
|
|
||||||
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
// Update button visibility based on model type
|
|
||||||
const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]');
|
|
||||||
const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]');
|
|
||||||
const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]');
|
|
||||||
const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]');
|
|
||||||
const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]');
|
|
||||||
|
|
||||||
if (sendToWorkflowBtn) {
|
|
||||||
sendToWorkflowBtn.style.display = config.sendToWorkflow ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (copyAllBtn) {
|
|
||||||
copyAllBtn.style.display = config.copyAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (refreshAllBtn) {
|
|
||||||
refreshAllBtn.style.display = config.refreshAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (moveAllBtn) {
|
|
||||||
moveAllBtn.style.display = config.moveAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (deleteAllBtn) {
|
|
||||||
deleteAllBtn.style.display = config.deleteAll ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,27 +104,10 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
});
|
});
|
||||||
state.selectedModels.clear();
|
state.selectedModels.clear();
|
||||||
this.updateSelectedCount();
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectedCount() {
|
|
||||||
const countElement = document.getElementById('selectedCount');
|
|
||||||
|
|
||||||
if (countElement) {
|
// Update context menu header if visible
|
||||||
// Use i18nHelpers.js to update the count text
|
if (this.bulkContextMenu) {
|
||||||
updateElementText(countElement, 'loras.bulkOperations.selected', { count: state.selectedModels.size });
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
|
|
||||||
const existingCaret = countElement.querySelector('.dropdown-caret');
|
|
||||||
if (existingCaret) {
|
|
||||||
existingCaret.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`;
|
|
||||||
existingCaret.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden';
|
|
||||||
} else {
|
|
||||||
const caretIcon = document.createElement('i');
|
|
||||||
caretIcon.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`;
|
|
||||||
caretIcon.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden';
|
|
||||||
countElement.appendChild(caretIcon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,10 +132,9 @@ export class BulkManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSelectedCount();
|
// Update context menu header if visible
|
||||||
|
if (this.bulkContextMenu) {
|
||||||
if (this.isStripVisible) {
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
this.updateThumbnailStrip();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,8 +186,6 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateSelectedCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyAllModelsSyntax() {
|
async copyAllModelsSyntax() {
|
||||||
@@ -413,115 +320,6 @@ export class BulkManager {
|
|||||||
showToast('toast.models.deleteFailedGeneral', {}, 'error');
|
showToast('toast.models.deleteFailedGeneral', {}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleThumbnailStrip() {
|
|
||||||
if (state.selectedModels.size === 0) return;
|
|
||||||
|
|
||||||
const existing = document.querySelector('.selected-thumbnails-strip');
|
|
||||||
if (existing) {
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
} else {
|
|
||||||
this.showThumbnailStrip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showThumbnailStrip() {
|
|
||||||
const strip = document.createElement('div');
|
|
||||||
strip.className = 'selected-thumbnails-strip';
|
|
||||||
|
|
||||||
const thumbnailContainer = document.createElement('div');
|
|
||||||
thumbnailContainer.className = 'thumbnails-container';
|
|
||||||
strip.appendChild(thumbnailContainer);
|
|
||||||
|
|
||||||
this.bulkPanel.parentNode.insertBefore(strip, this.bulkPanel);
|
|
||||||
|
|
||||||
this.updateThumbnailStrip();
|
|
||||||
|
|
||||||
this.isStripVisible = true;
|
|
||||||
this.updateSelectedCount();
|
|
||||||
|
|
||||||
setTimeout(() => strip.classList.add('visible'), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideThumbnailStrip() {
|
|
||||||
const strip = document.querySelector('.selected-thumbnails-strip');
|
|
||||||
if (strip && this.isStripVisible) {
|
|
||||||
strip.classList.remove('visible');
|
|
||||||
|
|
||||||
this.isStripVisible = false;
|
|
||||||
|
|
||||||
const countElement = document.getElementById('selectedCount');
|
|
||||||
if (countElement) {
|
|
||||||
const caret = countElement.querySelector('.dropdown-caret');
|
|
||||||
if (caret) {
|
|
||||||
caret.className = 'fas fa-caret-up dropdown-caret';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (strip.parentNode) {
|
|
||||||
strip.parentNode.removeChild(strip);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateThumbnailStrip() {
|
|
||||||
const container = document.querySelector('.thumbnails-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
const selectedModels = Array.from(state.selectedModels);
|
|
||||||
|
|
||||||
if (selectedModels.length > this.stripMaxThumbnails) {
|
|
||||||
const counter = document.createElement('div');
|
|
||||||
counter.className = 'strip-counter';
|
|
||||||
counter.textContent = `Showing ${this.stripMaxThumbnails} of ${selectedModels.length} selected`;
|
|
||||||
container.appendChild(counter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailsToShow = selectedModels.slice(0, this.stripMaxThumbnails);
|
|
||||||
const metadataCache = this.getMetadataCache();
|
|
||||||
|
|
||||||
thumbnailsToShow.forEach(filepath => {
|
|
||||||
const metadata = metadataCache.get(filepath);
|
|
||||||
if (!metadata) return;
|
|
||||||
|
|
||||||
const thumbnail = document.createElement('div');
|
|
||||||
thumbnail.className = 'selected-thumbnail';
|
|
||||||
thumbnail.dataset.filepath = filepath;
|
|
||||||
|
|
||||||
if (metadata.isVideo) {
|
|
||||||
thumbnail.innerHTML = `
|
|
||||||
<video autoplay loop muted playsinline>
|
|
||||||
<source src="${metadata.previewUrl}" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
<span class="thumbnail-name" title="${metadata.modelName}">${metadata.modelName}</span>
|
|
||||||
<button class="thumbnail-remove"><i class="fas fa-times"></i></button>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
thumbnail.innerHTML = `
|
|
||||||
<img src="${metadata.previewUrl}" alt="${metadata.modelName}">
|
|
||||||
<span class="thumbnail-name" title="${metadata.modelName}">${metadata.modelName}</span>
|
|
||||||
<button class="thumbnail-remove"><i class="fas fa-times"></i></button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbnail.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('.thumbnail-remove')) {
|
|
||||||
this.deselectItem(filepath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
thumbnail.querySelector('.thumbnail-remove').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.deselectItem(filepath);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(thumbnail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectItem(filepath) {
|
deselectItem(filepath) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||||
@@ -530,13 +328,6 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.selectedModels.delete(filepath);
|
state.selectedModels.delete(filepath);
|
||||||
|
|
||||||
this.updateSelectedCount();
|
|
||||||
this.updateThumbnailStrip();
|
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAllVisibleModels() {
|
selectAllVisibleModels() {
|
||||||
@@ -619,6 +410,330 @@ export class BulkManager {
|
|||||||
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBulkAddTagsModal() {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const countElement = document.getElementById('bulkAddTagsCount');
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = state.selectedModels.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing tags in the modal
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.showModal('bulkAddTagsModal', null, null, () => {
|
||||||
|
// Cleanup when modal is closed
|
||||||
|
this.cleanupBulkAddTagsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the bulk tags editing interface
|
||||||
|
this.initializeBulkTagsInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeBulkTagsInterface() {
|
||||||
|
// Setup tag input behavior
|
||||||
|
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (tagInput) {
|
||||||
|
tagInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.addBulkTag(e.target.value.trim());
|
||||||
|
e.target.value = '';
|
||||||
|
// Update dropdown to show added indicator
|
||||||
|
this.updateBulkSuggestionsDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create suggestions dropdown
|
||||||
|
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||||
|
if (tagForm) {
|
||||||
|
const suggestionsDropdown = this.createBulkSuggestionsDropdown(PRESET_TAGS);
|
||||||
|
tagForm.appendChild(suggestionsDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup save button
|
||||||
|
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
||||||
|
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
||||||
|
|
||||||
|
if (appendBtn) {
|
||||||
|
appendBtn.addEventListener('click', () => {
|
||||||
|
this.saveBulkTags('append');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceBtn) {
|
||||||
|
replaceBtn.addEventListener('click', () => {
|
||||||
|
this.saveBulkTags('replace');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createBulkSuggestionsDropdown(presetTags) {
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
dropdown.className = 'metadata-suggestions-dropdown';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'metadata-suggestions-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<span>Suggested Tags</span>
|
||||||
|
<small>Click to add</small>
|
||||||
|
`;
|
||||||
|
dropdown.appendChild(header);
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'metadata-suggestions-container';
|
||||||
|
|
||||||
|
presetTags.forEach(tag => {
|
||||||
|
// Check if tag is already added
|
||||||
|
const existingTags = this.getBulkExistingTags();
|
||||||
|
const isAdded = existingTags.includes(tag);
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||||
|
item.title = tag;
|
||||||
|
item.innerHTML = `
|
||||||
|
<span class="metadata-suggestion-text">${tag}</span>
|
||||||
|
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!isAdded) {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.addBulkTag(tag);
|
||||||
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = tag;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
// Update dropdown to show added indicator
|
||||||
|
this.updateBulkSuggestionsDropdown();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropdown.appendChild(container);
|
||||||
|
return dropdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
addBulkTag(tag) {
|
||||||
|
tag = tag.trim().toLowerCase();
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
|
// Validation: Check length
|
||||||
|
if (tag.length > 30) {
|
||||||
|
showToast('modelTags.validation.maxLength', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: Check total number
|
||||||
|
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
||||||
|
if (currentTags.length >= 30) {
|
||||||
|
showToast('modelTags.validation.maxCount', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: Check for duplicates
|
||||||
|
const existingTags = Array.from(currentTags).map(tagEl => tagEl.dataset.tag);
|
||||||
|
if (existingTags.includes(tag)) {
|
||||||
|
showToast('modelTags.validation.duplicate', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tag
|
||||||
|
const newTag = document.createElement('div');
|
||||||
|
newTag.className = 'metadata-item';
|
||||||
|
newTag.dataset.tag = tag;
|
||||||
|
newTag.innerHTML = `
|
||||||
|
<span class="metadata-item-content">${tag}</span>
|
||||||
|
<button class="metadata-delete-btn">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add delete button event listener
|
||||||
|
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
newTag.remove();
|
||||||
|
// Update dropdown to show/hide added indicator
|
||||||
|
this.updateBulkSuggestionsDropdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
tagsContainer.appendChild(newTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get existing tags in the bulk tags container
|
||||||
|
* @returns {Array} Array of existing tag strings
|
||||||
|
*/
|
||||||
|
getBulkExistingTags() {
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (!tagsContainer) return [];
|
||||||
|
|
||||||
|
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
||||||
|
return Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status of items in the bulk suggestions dropdown
|
||||||
|
*/
|
||||||
|
updateBulkSuggestionsDropdown() {
|
||||||
|
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
// Get all current tags
|
||||||
|
const existingTags = this.getBulkExistingTags();
|
||||||
|
|
||||||
|
// Update status of each item in dropdown
|
||||||
|
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
||||||
|
const tagText = item.querySelector('.metadata-suggestion-text').textContent;
|
||||||
|
const isAdded = existingTags.includes(tagText);
|
||||||
|
|
||||||
|
if (isAdded) {
|
||||||
|
item.classList.add('already-added');
|
||||||
|
|
||||||
|
// Add indicator if it doesn't exist
|
||||||
|
let indicator = item.querySelector('.added-indicator');
|
||||||
|
if (!indicator) {
|
||||||
|
indicator = document.createElement('span');
|
||||||
|
indicator.className = 'added-indicator';
|
||||||
|
indicator.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
item.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove click event
|
||||||
|
item.onclick = null;
|
||||||
|
item.removeEventListener('click', item._clickHandler);
|
||||||
|
} else {
|
||||||
|
// Re-enable items that are no longer in the list
|
||||||
|
item.classList.remove('already-added');
|
||||||
|
|
||||||
|
// Remove indicator if it exists
|
||||||
|
const indicator = item.querySelector('.added-indicator');
|
||||||
|
if (indicator) indicator.remove();
|
||||||
|
|
||||||
|
// Restore click event if not already set
|
||||||
|
if (!item._clickHandler) {
|
||||||
|
item._clickHandler = () => {
|
||||||
|
this.addBulkTag(tagText);
|
||||||
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = tagText;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
// Update dropdown to show added indicator
|
||||||
|
this.updateBulkSuggestionsDropdown();
|
||||||
|
};
|
||||||
|
item.addEventListener('click', item._clickHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBulkTags(mode = 'append') {
|
||||||
|
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
||||||
|
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||||
|
|
||||||
|
if (tags.length === 0) {
|
||||||
|
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
// Add or replace tags for each selected model based on mode
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
try {
|
||||||
|
if (mode === 'replace') {
|
||||||
|
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
||||||
|
} else {
|
||||||
|
await apiClient.addTags(filePath, { tags: tags });
|
||||||
|
}
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to ${mode} tags for ${filePath}:`, error);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.closeModal('bulkAddTagsModal');
|
||||||
|
|
||||||
|
if (successCount > 0) {
|
||||||
|
const currentConfig = MODEL_CONFIG[state.currentPageType];
|
||||||
|
const toastKey = mode === 'replace' ? 'toast.models.tagsReplacedSuccessfully' : 'toast.models.tagsAddedSuccessfully';
|
||||||
|
showToast(toastKey, {
|
||||||
|
count: successCount,
|
||||||
|
tagCount: tags.length,
|
||||||
|
type: currentConfig.displayName.toLowerCase()
|
||||||
|
}, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failCount > 0) {
|
||||||
|
const toastKey = mode === 'replace' ? 'toast.models.tagsReplaceFailed' : 'toast.models.tagsAddFailed';
|
||||||
|
showToast(toastKey, { count: failCount }, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during bulk tag operation:', error);
|
||||||
|
const toastKey = mode === 'replace' ? 'toast.models.bulkTagsReplaceFailed' : 'toast.models.bulkTagsAddFailed';
|
||||||
|
showToast(toastKey, {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupBulkAddTagsModal() {
|
||||||
|
// Clear tags container
|
||||||
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove event listeners (they will be re-added when modal opens again)
|
||||||
|
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
||||||
|
if (appendBtn) {
|
||||||
|
appendBtn.replaceWith(appendBtn.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
||||||
|
if (replaceBtn) {
|
||||||
|
replaceBtn.replaceWith(replaceBtn.cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the suggestions dropdown
|
||||||
|
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||||
|
if (tagForm) {
|
||||||
|
const dropdown = tagForm.querySelector('.metadata-suggestions-dropdown');
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkManager = new BulkManager();
|
export const bulkManager = new BulkManager();
|
||||||
|
|||||||
@@ -234,6 +234,19 @@ export class ModalManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add bulkAddTagsModal registration
|
||||||
|
const bulkAddTagsModal = document.getElementById('bulkAddTagsModal');
|
||||||
|
if (bulkAddTagsModal) {
|
||||||
|
this.registerModal('bulkAddTagsModal', {
|
||||||
|
element: bulkAddTagsModal,
|
||||||
|
onClose: () => {
|
||||||
|
this.getModal('bulkAddTagsModal').element.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
},
|
||||||
|
closeOnOutsideClick: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', this.boundHandleEscape);
|
document.addEventListener('keydown', this.boundHandleEscape);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export const state = {
|
|||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
showFavoritesOnly: false,
|
showFavoritesOnly: false,
|
||||||
duplicatesMode: false,
|
duplicatesMode: false,
|
||||||
|
bulkMode: false,
|
||||||
|
selectedModels: new Set(),
|
||||||
},
|
},
|
||||||
|
|
||||||
[MODEL_TYPES.CHECKPOINT]: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
|
|||||||
@@ -163,3 +163,10 @@ export const NODE_TYPE_ICONS = {
|
|||||||
|
|
||||||
// Default ComfyUI node color when bgcolor is null
|
// Default ComfyUI node color when bgcolor is null
|
||||||
export const DEFAULT_NODE_COLOR = "#353535";
|
export const DEFAULT_NODE_COLOR = "#353535";
|
||||||
|
|
||||||
|
// Preset tag suggestions
|
||||||
|
export const PRESET_TAGS = [
|
||||||
|
'character', 'style', 'concept', 'clothing',
|
||||||
|
'poses', 'background', 'vehicle', 'buildings',
|
||||||
|
'objects', 'animal'
|
||||||
|
];
|
||||||
|
|||||||
@@ -44,6 +44,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="bulkContextMenu" class="context-menu">
|
||||||
|
<div class="bulk-context-header">
|
||||||
|
<i class="fas fa-th-large"></i>
|
||||||
|
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item" data-action="add-tags">
|
||||||
|
<i class="fas fa-tags"></i> <span>{{ t('loras.bulkOperations.addTags') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="send-to-workflow">
|
||||||
|
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="copy-all">
|
||||||
|
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="refresh-all">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="move-all">
|
||||||
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item delete-item" data-action="delete-all">
|
||||||
|
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item" data-action="clear">
|
||||||
|
<i class="fas fa-times"></i> <span>{{ t('loras.bulkOperations.clear') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
||||||
<div class="nsfw-level-header">
|
<div class="nsfw-level-header">
|
||||||
<h3>{{ t('modals.contentRating.title') }}</h3>
|
<h3>{{ t('modals.contentRating.title') }}</h3>
|
||||||
|
|||||||
@@ -98,33 +98,4 @@
|
|||||||
<!-- Breadcrumbs will be populated by JavaScript -->
|
<!-- Breadcrumbs will be populated by JavaScript -->
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add bulk operations panel (initially hidden) -->
|
|
||||||
<div id="bulkOperationsPanel" class="bulk-operations-panel hidden">
|
|
||||||
<div class="bulk-operations-header">
|
|
||||||
<span id="selectedCount" class="selectable-count" title="{{ t('loras.bulkOperations.viewSelected') }}">
|
|
||||||
0 {{ t('loras.bulkOperations.selectedSuffix') }} <i class="fas fa-caret-down dropdown-caret"></i>
|
|
||||||
</span>
|
|
||||||
<div class="bulk-operations-actions">
|
|
||||||
<button data-action="send-to-workflow" title="{{ t('loras.bulkOperations.sendToWorkflow') }}">
|
|
||||||
<i class="fas fa-arrow-right"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="copy-all" title="{{ t('loras.bulkOperations.copyAll') }}">
|
|
||||||
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="refresh-all" title="{{ t('loras.bulkOperations.refreshAll') }}">
|
|
||||||
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="move-all" title="{{ t('loras.bulkOperations.moveAll') }}">
|
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="delete-all" title="{{ t('loras.bulkOperations.deleteAll') }}" class="danger-btn">
|
|
||||||
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="clear" title="{{ t('loras.bulkOperations.clear') }}">
|
|
||||||
<i class="fas fa-times"></i> <span>{{ t('loras.bulkOperations.clear') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -9,4 +9,5 @@
|
|||||||
{% include 'components/modals/relink_civitai_modal.html' %}
|
{% include 'components/modals/relink_civitai_modal.html' %}
|
||||||
{% include 'components/modals/example_access_modal.html' %}
|
{% include 'components/modals/example_access_modal.html' %}
|
||||||
{% include 'components/modals/download_modal.html' %}
|
{% include 'components/modals/download_modal.html' %}
|
||||||
{% include 'components/modals/move_modal.html' %}
|
{% include 'components/modals/move_modal.html' %}
|
||||||
|
{% include 'components/modals/bulk_add_tags_modal.html' %}
|
||||||
38
templates/components/modals/bulk_add_tags_modal.html
Normal file
38
templates/components/modals/bulk_add_tags_modal.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<div id="bulkAddTagsModal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content modal-content-large">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>{{ t('modals.bulkAddTags.title') }}</h2>
|
||||||
|
<span class="close" onclick="modalManager.closeModal('bulkAddTagsModal')">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="bulk-add-tags-info">
|
||||||
|
<p>{{ t('modals.bulkAddTags.description') }} <span id="bulkAddTagsCount">0</span> {{ t('modals.bulkAddTags.models') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="model-tags-container bulk-tags-container edit-mode">
|
||||||
|
<div class="metadata-edit-container" style="display: block;">
|
||||||
|
<div class="metadata-edit-content">
|
||||||
|
<div class="metadata-edit-header">
|
||||||
|
<label>{{ t('modals.bulkAddTags.tagsToAdd') }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-items" id="bulkTagsItems">
|
||||||
|
<!-- Tags will be added here dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="metadata-edit-controls">
|
||||||
|
<button class="append-tags-btn bulk-append-tags-btn" title="{{ t('modals.bulkAddTags.appendTags') }}">
|
||||||
|
<i class="fas fa-plus"></i> {{ t('modals.bulkAddTags.appendTags') }}
|
||||||
|
</button>
|
||||||
|
<button class="replace-tags-btn bulk-replace-tags-btn" title="{{ t('modals.bulkAddTags.replaceTags') }}">
|
||||||
|
<i class="fas fa-exchange-alt"></i> {{ t('modals.bulkAddTags.replaceTags') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="metadata-add-form">
|
||||||
|
<input type="text" class="metadata-input bulk-metadata-input" placeholder="{{ t('modals.bulkAddTags.placeholder') }}">
|
||||||
|
</div>
|
||||||
|
<!-- Suggestions dropdown will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
<div class="card-grid" id="modelGrid">
|
<div class="card-grid" id="modelGrid">
|
||||||
<!-- Cards will be dynamically inserted here -->
|
<!-- Cards will be dynamically inserted here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block overlay %}
|
{% block overlay %}
|
||||||
|
|||||||
Reference in New Issue
Block a user