From c2716e3c39e2effe5285b4c82fb350d84c865ad1 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 2 Mar 2026 22:02:47 +0800 Subject: [PATCH] fix(i18n): resolve missing translation keys and complete multi-language support - Add missing keys 'common.cancel', 'common.confirm', and 'sidebar.dragDrop.noDragState' to en.json - Synchronize all locale files using sync_translation_keys.py - Complete translations for zh-CN, zh-TW, ja, ru, de, fr, es, ko, and he - Implement sidebar drag-and-drop folder creation with visual feedback and input validation - Optimize MoveManager to use resetAndReload for consistent UI state after moving models - Fix recursive visibility check for root folder in MoveManager --- locales/de.json | 51 +-- locales/en.json | 15 +- locales/es.json | 51 +-- locales/fr.json | 51 +-- locales/he.json | 65 ++-- locales/ja.json | 53 +-- locales/ko.json | 51 +-- locales/ru.json | 51 +-- locales/zh-CN.json | 51 +-- locales/zh-TW.json | 51 +-- static/css/components/sidebar.css | 168 ++++++++++ static/js/components/SidebarManager.js | 436 ++++++++++++++++++++++++- static/js/managers/MoveManager.js | 89 ++--- 13 files changed, 921 insertions(+), 262 deletions(-) diff --git a/locales/de.json b/locales/de.json index ed31bb8f..6baf24a8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "Abbrechen", + "confirm": "Bestätigen", "actions": { "save": "Speichern", "cancel": "Abbrechen", + "confirm": "Bestätigen", "delete": "Löschen", "move": "Verschieben", "refresh": "Aktualisieren", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "Legen Sie den Standard-Embedding-Stammordner für Downloads, Importe und Verschiebungen fest", "noDefault": "Kein Standard" }, + "extraFolderPaths": { + "title": "Zusätzliche Ordnerpfade", + "help": "Fügen Sie zusätzliche Modellordner außerhalb der Standardpfade von ComfyUI hinzu. Diese Pfade werden separat gespeichert und zusammen mit den Standardordnern gescannt.", + "description": "Konfigurieren Sie zusätzliche Ordner zum Scannen von Modellen. Diese Pfade sind spezifisch für LoRA Manager und werden mit den Standardpfaden von ComfyUI zusammengeführt.", + "modelTypes": { + "lora": "LoRA-Pfade", + "checkpoint": "Checkpoint-Pfade", + "unet": "Diffusionsmodell-Pfade", + "embedding": "Embedding-Pfade" + }, + "pathPlaceholder": "/pfad/zu/extra/modellen", + "saveSuccess": "Zusätzliche Ordnerpfade aktualisiert.", + "saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}", + "validation": { + "duplicatePath": "Dieser Pfad ist bereits konfiguriert" + } + }, "priorityTags": { "title": "Prioritäts-Tags", "description": "Passen Sie die Tag-Prioritätsreihenfolge für jeden Modelltyp an (z. B. character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "Passwort (optional)", "proxyPasswordPlaceholder": "passwort", "proxyPasswordHelp": "Passwort für die Proxy-Authentifizierung (falls erforderlich)" - }, - "extraFolderPaths": { - "title": "Zusätzliche Ordnerpfade", - "help": "Fügen Sie zusätzliche Modellordner außerhalb der Standardpfade von ComfyUI hinzu. Diese Pfade werden separat gespeichert und zusammen mit den Standardordnern gescannt.", - "description": "Konfigurieren Sie zusätzliche Ordner zum Scannen von Modellen. Diese Pfade sind spezifisch für LoRA Manager und werden mit den Standardpfaden von ComfyUI zusammengeführt.", - "modelTypes": { - "lora": "LoRA-Pfade", - "checkpoint": "Checkpoint-Pfade", - "unet": "Diffusionsmodell-Pfade", - "embedding": "Embedding-Pfade" - }, - "pathPlaceholder": "/pfad/zu/extra/modellen", - "saveSuccess": "Zusätzliche Ordnerpfade aktualisiert.", - "saveError": "Fehler beim Aktualisieren der zusätzlichen Ordnerpfade: {message}", - "validation": { - "duplicatePath": "Dieser Pfad ist bereits konfiguriert" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "Im Listenmodus nicht verfügbar", "dragDrop": { "unableToResolveRoot": "Zielpfad für das Verschieben konnte nicht ermittelt werden.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Verschieben wird für dieses Element nicht unterstützt.", + "createFolderHint": "Loslassen, um einen neuen Ordner zu erstellen", + "newFolderName": "Neuer Ordnername", + "folderNameHint": "Eingabetaste zum Bestätigen, Escape zum Abbrechen", + "emptyFolderName": "Bitte geben Sie einen Ordnernamen ein", + "invalidFolderName": "Ordnername enthält ungültige Zeichen", + "noDragState": "Kein ausstehender Ziehvorgang gefunden" + }, + "empty": { + "noFolders": "Keine Ordner gefunden", + "dragHint": "Elemente hierher ziehen, um Ordner zu erstellen" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "Wiederholen" } } -} \ No newline at end of file +} diff --git a/locales/en.json b/locales/en.json index 40f3f9b9..6aa007ff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "Cancel", + "confirm": "Confirm", "actions": { "save": "Save", "cancel": "Cancel", + "confirm": "Confirm", "delete": "Delete", "move": "Move", "refresh": "Refresh", @@ -750,7 +753,17 @@ "collapseAllDisabled": "Not available in list view", "dragDrop": { "unableToResolveRoot": "Unable to determine destination path for move.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Move is not supported for this item.", + "createFolderHint": "Release to create new folder", + "newFolderName": "New folder name", + "folderNameHint": "Press Enter to confirm, Escape to cancel", + "emptyFolderName": "Please enter a folder name", + "invalidFolderName": "Folder name contains invalid characters", + "noDragState": "No pending drag operation found" + }, + "empty": { + "noFolders": "No folders found", + "dragHint": "Drag items here to create folders" } }, "statistics": { diff --git a/locales/es.json b/locales/es.json index 06ddaebc..7b96f41d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "Cancelar", + "confirm": "Confirmar", "actions": { "save": "Guardar", "cancel": "Cancelar", + "confirm": "Confirmar", "delete": "Eliminar", "move": "Mover", "refresh": "Actualizar", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "Establecer el directorio raíz predeterminado de embedding para descargas, importaciones y movimientos", "noDefault": "Sin predeterminado" }, + "extraFolderPaths": { + "title": "Rutas de carpetas adicionales", + "help": "Agregue carpetas de modelos adicionales fuera de las rutas estándar de ComfyUI. Estas rutas se almacenan por separado y se escanean junto con las carpetas predeterminadas.", + "description": "Configure carpetas adicionales para escanear modelos. Estas rutas son específicas de LoRA Manager y se fusionarán con las rutas predeterminadas de ComfyUI.", + "modelTypes": { + "lora": "Rutas de LoRA", + "checkpoint": "Rutas de Checkpoint", + "unet": "Rutas de modelo de difusión", + "embedding": "Rutas de Embedding" + }, + "pathPlaceholder": "/ruta/a/modelos/extra", + "saveSuccess": "Rutas de carpetas adicionales actualizadas.", + "saveError": "Error al actualizar las rutas de carpetas adicionales: {message}", + "validation": { + "duplicatePath": "Esta ruta ya está configurada" + } + }, "priorityTags": { "title": "Etiquetas prioritarias", "description": "Personaliza el orden de prioridad de etiquetas para cada tipo de modelo (p. ej., character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "Contraseña (opcional)", "proxyPasswordPlaceholder": "contraseña", "proxyPasswordHelp": "Contraseña para autenticación de proxy (si es necesario)" - }, - "extraFolderPaths": { - "title": "Rutas de carpetas adicionales", - "help": "Agregue carpetas de modelos adicionales fuera de las rutas estándar de ComfyUI. Estas rutas se almacenan por separado y se escanean junto con las carpetas predeterminadas.", - "description": "Configure carpetas adicionales para escanear modelos. Estas rutas son específicas de LoRA Manager y se fusionarán con las rutas predeterminadas de ComfyUI.", - "modelTypes": { - "lora": "Rutas de LoRA", - "checkpoint": "Rutas de Checkpoint", - "unet": "Rutas de modelo de difusión", - "embedding": "Rutas de Embedding" - }, - "pathPlaceholder": "/ruta/a/modelos/extra", - "saveSuccess": "Rutas de carpetas adicionales actualizadas.", - "saveError": "Error al actualizar las rutas de carpetas adicionales: {message}", - "validation": { - "duplicatePath": "Esta ruta ya está configurada" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "No disponible en vista de lista", "dragDrop": { "unableToResolveRoot": "No se puede determinar la ruta de destino para el movimiento.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "El movimiento no es compatible con este elemento.", + "createFolderHint": "Suelta para crear una nueva carpeta", + "newFolderName": "Nombre de la nueva carpeta", + "folderNameHint": "Presiona Enter para confirmar, Escape para cancelar", + "emptyFolderName": "Por favor, introduce un nombre de carpeta", + "invalidFolderName": "El nombre de la carpeta contiene caracteres no válidos", + "noDragState": "No se encontró ninguna operación de arrastre pendiente" + }, + "empty": { + "noFolders": "No se encontraron carpetas", + "dragHint": "Arrastra elementos aquí para crear carpetas" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "Reintentar" } } -} \ No newline at end of file +} diff --git a/locales/fr.json b/locales/fr.json index 560c73f7..ceb0eebf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "Annuler", + "confirm": "Confirmer", "actions": { "save": "Enregistrer", "cancel": "Annuler", + "confirm": "Confirmer", "delete": "Supprimer", "move": "Déplacer", "refresh": "Actualiser", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "Définir le répertoire racine embedding par défaut pour les téléchargements, imports et déplacements", "noDefault": "Aucun par défaut" }, + "extraFolderPaths": { + "title": "Chemins de dossiers supplémentaires", + "help": "Ajoutez des dossiers de modèles supplémentaires en dehors des chemins standard de ComfyUI. Ces chemins sont stockés séparément et analysés aux côtés des dossiers par défaut.", + "description": "Configurez des dossiers supplémentaires pour l'analyse de modèles. Ces chemins sont spécifiques à LoRA Manager et seront fusionnés avec les chemins par défaut de ComfyUI.", + "modelTypes": { + "lora": "Chemins LoRA", + "checkpoint": "Chemins Checkpoint", + "unet": "Chemins de modèle de diffusion", + "embedding": "Chemins Embedding" + }, + "pathPlaceholder": "/chemin/vers/modèles/supplémentaires", + "saveSuccess": "Chemins de dossiers supplémentaires mis à jour.", + "saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}", + "validation": { + "duplicatePath": "Ce chemin est déjà configuré" + } + }, "priorityTags": { "title": "Étiquettes prioritaires", "description": "Personnalisez l'ordre de priorité des étiquettes pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "Mot de passe (optionnel)", "proxyPasswordPlaceholder": "mot_de_passe", "proxyPasswordHelp": "Mot de passe pour l'authentification proxy (si nécessaire)" - }, - "extraFolderPaths": { - "title": "Chemins de dossiers supplémentaires", - "help": "Ajoutez des dossiers de modèles supplémentaires en dehors des chemins standard de ComfyUI. Ces chemins sont stockés séparément et analysés aux côtés des dossiers par défaut.", - "description": "Configurez des dossiers supplémentaires pour l'analyse de modèles. Ces chemins sont spécifiques à LoRA Manager et seront fusionnés avec les chemins par défaut de ComfyUI.", - "modelTypes": { - "lora": "Chemins LoRA", - "checkpoint": "Chemins Checkpoint", - "unet": "Chemins de modèle de diffusion", - "embedding": "Chemins Embedding" - }, - "pathPlaceholder": "/chemin/vers/modèles/supplémentaires", - "saveSuccess": "Chemins de dossiers supplémentaires mis à jour.", - "saveError": "Échec de la mise à jour des chemins de dossiers supplémentaires: {message}", - "validation": { - "duplicatePath": "Ce chemin est déjà configuré" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "Non disponible en vue liste", "dragDrop": { "unableToResolveRoot": "Impossible de déterminer le chemin de destination pour le déplacement.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Le déplacement n'est pas pris en charge pour cet élément.", + "createFolderHint": "Relâcher pour créer un nouveau dossier", + "newFolderName": "Nom du nouveau dossier", + "folderNameHint": "Appuyez sur Entrée pour confirmer, Échap pour annuler", + "emptyFolderName": "Veuillez saisir un nom de dossier", + "invalidFolderName": "Le nom du dossier contient des caractères invalides", + "noDragState": "Aucune opération de glissement en attente trouvée" + }, + "empty": { + "noFolders": "Aucun dossier trouvé", + "dragHint": "Faites glisser des éléments ici pour créer des dossiers" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "Réessayer" } } -} \ No newline at end of file +} diff --git a/locales/he.json b/locales/he.json index f9ce3965..51e89a26 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1,17 +1,20 @@ { "common": { + "cancel": "ביטול", + "confirm": "אישור", "actions": { - "save": "שמור", + "save": "שמירה", "cancel": "ביטול", - "delete": "מחק", - "move": "העבר", - "refresh": "רענן", - "back": "חזור", + "confirm": "אישור", + "delete": "מחיקה", + "move": "העברה", + "refresh": "רענון", + "back": "חזרה", "next": "הבא", - "backToTop": "חזור למעלה", + "backToTop": "חזרה למעלה", "settings": "הגדרות", "help": "עזרה", - "add": "הוסף" + "add": "הוספה" }, "status": { "loading": "טוען...", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של embedding להורדות, ייבוא והעברות", "noDefault": "אין ברירת מחדל" }, + "extraFolderPaths": { + "title": "נתיבי תיקיות נוספים", + "help": "הוסף תיקיות מודלים נוספות מחוץ לנתיבים הסטנדרטיים של ComfyUI. נתיבים אלה נשמרים בנפרד ונסרקים לצד תיקיות ברירת המחדל.", + "description": "הגדר תיקיות נוספות לסריקת מודלים. נתיבים אלה ספציפיים ל-LoRA Manager וימוזגו עם נתיבי ברירת המחדל של ComfyUI.", + "modelTypes": { + "lora": "נתיבי LoRA", + "checkpoint": "נתיבי Checkpoint", + "unet": "נתיבי מודל דיפוזיה", + "embedding": "נתיבי Embedding" + }, + "pathPlaceholder": "/נתיב/למודלים/נוספים", + "saveSuccess": "נתיבי תיקיות נוספים עודכנו.", + "saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}", + "validation": { + "duplicatePath": "נתיב זה כבר מוגדר" + } + }, "priorityTags": { "title": "תגיות עדיפות", "description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "סיסמה (אופציונלי)", "proxyPasswordPlaceholder": "password", "proxyPasswordHelp": "סיסמה לאימות מול הפרוקסי (אם נדרש)" - }, - "extraFolderPaths": { - "title": "נתיבי תיקיות נוספים", - "help": "הוסף תיקיות מודלים נוספות מחוץ לנתיבים הסטנדרטיים של ComfyUI. נתיבים אלה נשמרים בנפרד ונסרקים לצד תיקיות ברירת המחדל.", - "description": "הגדר תיקיות נוספות לסריקת מודלים. נתיבים אלה ספציפיים ל-LoRA Manager וימוזגו עם נתיבי ברירת המחדל של ComfyUI.", - "modelTypes": { - "lora": "נתיבי LoRA", - "checkpoint": "נתיבי Checkpoint", - "unet": "נתיבי מודל דיפוזיה", - "embedding": "נתיבי Embedding" - }, - "pathPlaceholder": "/נתיב/למודלים/נוספים", - "saveSuccess": "נתיבי תיקיות נוספים עודכנו.", - "saveError": "נכשל בעדכון נתיבי תיקיות נוספים: {message}", - "validation": { - "duplicatePath": "נתיב זה כבר מוגדר" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "לא זמין בתצוגת רשימה", "dragDrop": { "unableToResolveRoot": "לא ניתן לקבוע את נתיב היעד להעברה.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "העברה אינה נתמכת עבור פריט זה.", + "createFolderHint": "שחרר כדי ליצור תיקייה חדשה", + "newFolderName": "שם תיקייה חדשה", + "folderNameHint": "הקש Enter לאישור, Escape לביטול", + "emptyFolderName": "אנא הזן שם תיקייה", + "invalidFolderName": "שם התיקייה מכיל תווים לא חוקיים", + "noDragState": "לא נמצאה פעולת גרירה ממתינה" + }, + "empty": { + "noFolders": "לא נמצאו תיקיות", + "dragHint": "גרור פריטים לכאן כדי ליצור תיקיות" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "נסה שוב" } } -} \ No newline at end of file +} diff --git a/locales/ja.json b/locales/ja.json index db5705a8..1130fbf6 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,14 +1,17 @@ { "common": { + "cancel": "キャンセル", + "confirm": "確認", "actions": { "save": "保存", "cancel": "キャンセル", + "confirm": "確認", "delete": "削除", "move": "移動", "refresh": "更新", "back": "戻る", "next": "次へ", - "backToTop": "トップに戻る", + "backToTop": "トップへ戻る", "settings": "設定", "help": "ヘルプ", "add": "追加" @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "ダウンロード、インポート、移動用のデフォルトembeddingルートディレクトリを設定", "noDefault": "デフォルトなし" }, + "extraFolderPaths": { + "title": "追加フォルダーパス", + "help": "ComfyUIの標準パスの外部に追加のモデルフォルダを追加します。これらのパスは別々に保存され、デフォルトのフォルダと一緒にスキャンされます。", + "description": "モデルをスキャンするための追加フォルダを設定します。これらのパスはLoRA Manager固有であり、ComfyUIのデフォルトパスとマージされます。", + "modelTypes": { + "lora": "LoRAパス", + "checkpoint": "Checkpointパス", + "unet": "Diffusionモデルパス", + "embedding": "Embeddingパス" + }, + "pathPlaceholder": "/追加モデルへのパス", + "saveSuccess": "追加フォルダーパスを更新しました。", + "saveError": "追加フォルダーパスの更新に失敗しました: {message}", + "validation": { + "duplicatePath": "このパスはすでに設定されています" + } + }, "priorityTags": { "title": "優先タグ", "description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "パスワード(任意)", "proxyPasswordPlaceholder": "パスワード", "proxyPasswordHelp": "プロキシ認証用のパスワード(必要な場合)" - }, - "extraFolderPaths": { - "title": "追加フォルダーパス", - "help": "ComfyUIの標準パスの外部に追加のモデルフォルダを追加します。これらのパスは別々に保存され、デフォルトのフォルダと一緒にスキャンされます。", - "description": "モデルをスキャンするための追加フォルダを設定します。これらのパスはLoRA Manager固有であり、ComfyUIのデフォルトパスとマージされます。", - "modelTypes": { - "lora": "LoRAパス", - "checkpoint": "Checkpointパス", - "unet": "Diffusionモデルパス", - "embedding": "Embeddingパス" - }, - "pathPlaceholder": "/追加モデルへのパス", - "saveSuccess": "追加フォルダーパスを更新しました。", - "saveError": "追加フォルダーパスの更新に失敗しました: {message}", - "validation": { - "duplicatePath": "このパスはすでに設定されています" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "リストビューでは利用できません", "dragDrop": { "unableToResolveRoot": "移動先のパスを特定できません。", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "この項目の移動はサポートされていません。", + "createFolderHint": "放して新しいフォルダを作成", + "newFolderName": "新しいフォルダ名", + "folderNameHint": "Enterで確定、Escでキャンセル", + "emptyFolderName": "フォルダ名を入力してください", + "invalidFolderName": "フォルダ名に無効な文字が含まれています", + "noDragState": "保留中のドラッグ操作が見つかりません" + }, + "empty": { + "noFolders": "フォルダが見つかりません", + "dragHint": "ここへアイテムをドラッグしてフォルダを作成します" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "再試行" } } -} \ No newline at end of file +} diff --git a/locales/ko.json b/locales/ko.json index 92b6de8b..739ac6cd 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "취소", + "confirm": "확인", "actions": { "save": "저장", "cancel": "취소", + "confirm": "확인", "delete": "삭제", "move": "이동", "refresh": "새로고침", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Embedding 루트 디렉토리를 설정합니다", "noDefault": "기본값 없음" }, + "extraFolderPaths": { + "title": "추가 폴다 경로", + "help": "ComfyUI의 표준 경로 외부에 추가 모델 폴드를 추가하세요. 이러한 경로는 별도로 저장되며 기본 폴와 함께 스캔됩니다.", + "description": "모델을 스캔하기 위한 추가 폴를 설정하세요. 이러한 경로는 LoRA Manager 특유의 것이며 ComfyUI의 기본 경로와 병합됩니다.", + "modelTypes": { + "lora": "LoRA 경로", + "checkpoint": "Checkpoint 경로", + "unet": "Diffusion 모델 경로", + "embedding": "Embedding 경로" + }, + "pathPlaceholder": "/추가/모델/경로", + "saveSuccess": "추가 폴다 경로가 업데이트되었습니다.", + "saveError": "추가 폴다 경로 업데이트 실패: {message}", + "validation": { + "duplicatePath": "이 경로는 이미 구성되어 있습니다" + } + }, "priorityTags": { "title": "우선순위 태그", "description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).", @@ -485,23 +505,6 @@ "proxyPassword": "비밀번호 (선택사항)", "proxyPasswordPlaceholder": "password", "proxyPasswordHelp": "프록시 인증에 필요한 비밀번호 (필요한 경우)" - }, - "extraFolderPaths": { - "title": "추가 폴다 경로", - "help": "ComfyUI의 표준 경로 외부에 추가 모델 폴드를 추가하세요. 이러한 경로는 별도로 저장되며 기본 폴와 함께 스캔됩니다.", - "description": "모델을 스캔하기 위한 추가 폴를 설정하세요. 이러한 경로는 LoRA Manager 특유의 것이며 ComfyUI의 기본 경로와 병합됩니다.", - "modelTypes": { - "lora": "LoRA 경로", - "checkpoint": "Checkpoint 경로", - "unet": "Diffusion 모델 경로", - "embedding": "Embedding 경로" - }, - "pathPlaceholder": "/추가/모델/경로", - "saveSuccess": "추가 폴다 경로가 업데이트되었습니다.", - "saveError": "추가 폴다 경로 업데이트 실패: {message}", - "validation": { - "duplicatePath": "이 경로는 이미 구성되어 있습니다" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "목록 보기에서는 사용할 수 없습니다", "dragDrop": { "unableToResolveRoot": "이동할 대상 경로를 확인할 수 없습니다.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "이 항목은 이동을 지원하지 않습니다.", + "createFolderHint": "놓아서 새 폴더 만들기", + "newFolderName": "새 폴더 이름", + "folderNameHint": "Enter를 눌러 확인, Escape를 눌러 취소", + "emptyFolderName": "폴더 이름을 입력하세요", + "invalidFolderName": "폴더 이름에 잘못된 문자가 포함되어 있습니다", + "noDragState": "보류 중인 드래그 작업을 찾을 수 없습니다" + }, + "empty": { + "noFolders": "폴더를 찾을 수 없습니다", + "dragHint": "항목을 여기로 드래그하여 폴더를 만듭니다" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "다시 시도" } } -} \ No newline at end of file +} diff --git a/locales/ru.json b/locales/ru.json index 4dcbdbdb..40959469 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "Отмена", + "confirm": "Подтвердить", "actions": { "save": "Сохранить", "cancel": "Отмена", + "confirm": "Подтвердить", "delete": "Удалить", "move": "Переместить", "refresh": "Обновить", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "Установить корневую папку embedding по умолчанию для загрузок, импорта и перемещений", "noDefault": "Не задано" }, + "extraFolderPaths": { + "title": "Дополнительные пути к папкам", + "help": "Добавьте дополнительные папки моделей за пределами стандартных путей ComfyUI. Эти пути хранятся отдельно и сканируются вместе с папками по умолчанию.", + "description": "Настройте дополнительные папки для сканирования моделей. Эти пути специфичны для LoRA Manager и будут объединены с путями по умолчанию ComfyUI.", + "modelTypes": { + "lora": "Пути LoRA", + "checkpoint": "Пути Checkpoint", + "unet": "Пути моделей диффузии", + "embedding": "Пути Embedding" + }, + "pathPlaceholder": "/путь/к/дополнительным/моделям", + "saveSuccess": "Дополнительные пути к папкам обновлены.", + "saveError": "Не удалось обновить дополнительные пути к папкам: {message}", + "validation": { + "duplicatePath": "Этот путь уже настроен" + } + }, "priorityTags": { "title": "Приоритетные теги", "description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).", @@ -485,23 +505,6 @@ "proxyPassword": "Пароль (необязательно)", "proxyPasswordPlaceholder": "пароль", "proxyPasswordHelp": "Пароль для аутентификации на прокси (если требуется)" - }, - "extraFolderPaths": { - "title": "Дополнительные пути к папкам", - "help": "Добавьте дополнительные папки моделей за пределами стандартных путей ComfyUI. Эти пути хранятся отдельно и сканируются вместе с папками по умолчанию.", - "description": "Настройте дополнительные папки для сканирования моделей. Эти пути специфичны для LoRA Manager и будут объединены с путями по умолчанию ComfyUI.", - "modelTypes": { - "lora": "Пути LoRA", - "checkpoint": "Пути Checkpoint", - "unet": "Пути моделей диффузии", - "embedding": "Пути Embedding" - }, - "pathPlaceholder": "/путь/к/дополнительным/моделям", - "saveSuccess": "Дополнительные пути к папкам обновлены.", - "saveError": "Не удалось обновить дополнительные пути к папкам: {message}", - "validation": { - "duplicatePath": "Этот путь уже настроен" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "Недоступно в виде списка", "dragDrop": { "unableToResolveRoot": "Не удалось определить путь назначения для перемещения.", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Перемещение этого элемента не поддерживается.", + "createFolderHint": "Отпустите, чтобы создать новую папку", + "newFolderName": "Имя новой папки", + "folderNameHint": "Нажмите Enter для подтверждения, Escape для отмены", + "emptyFolderName": "Пожалуйста, введите имя папки", + "invalidFolderName": "Имя папки содержит недопустимые символы", + "noDragState": "Ожидающая операция перетаскивания не найдена" + }, + "empty": { + "noFolders": "Папки не найдены", + "dragHint": "Перетащите элементы сюда, чтобы создать папки" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "Повторить" } } -} \ No newline at end of file +} diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 843d66de..97f9f8d7 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "取消", + "confirm": "确认", "actions": { "save": "保存", "cancel": "取消", + "confirm": "确认", "delete": "删除", "move": "移动", "refresh": "刷新", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "设置下载、导入和移动时的默认 Embedding 根目录", "noDefault": "无默认" }, + "extraFolderPaths": { + "title": "额外文件夹路径", + "help": "在 ComfyUI 的标准路径之外添加额外的模型文件夹。这些路径单独存储,并与默认文件夹一起扫描。", + "description": "配置额外的文件夹以扫描模型。这些路径是 LoRA Manager 特有的,将与 ComfyUI 的默认路径合并。", + "modelTypes": { + "lora": "LoRA 路径", + "checkpoint": "Checkpoint 路径", + "unet": "Diffusion 模型路径", + "embedding": "Embedding 路径" + }, + "pathPlaceholder": "/额外/模型/路径", + "saveSuccess": "额外文件夹路径已更新。", + "saveError": "更新额外文件夹路径失败:{message}", + "validation": { + "duplicatePath": "此路径已配置" + } + }, "priorityTags": { "title": "优先标签", "description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "密码 (可选)", "proxyPasswordPlaceholder": "密码", "proxyPasswordHelp": "代理认证的密码 (如果需要)" - }, - "extraFolderPaths": { - "title": "额外文件夹路径", - "help": "在 ComfyUI 的标准路径之外添加额外的模型文件夹。这些路径单独存储,并与默认文件夹一起扫描。", - "description": "配置额外的文件夹以扫描模型。这些路径是 LoRA Manager 特有的,将与 ComfyUI 的默认路径合并。", - "modelTypes": { - "lora": "LoRA 路径", - "checkpoint": "Checkpoint 路径", - "unet": "Diffusion 模型路径", - "embedding": "Embedding 路径" - }, - "pathPlaceholder": "/额外/模型/路径", - "saveSuccess": "额外文件夹路径已更新。", - "saveError": "更新额外文件夹路径失败:{message}", - "validation": { - "duplicatePath": "此路径已配置" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "列表视图下不可用", "dragDrop": { "unableToResolveRoot": "无法确定移动的目标路径。", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Move is not supported for this item.", + "createFolderHint": "释放以创建新文件夹", + "newFolderName": "新文件夹名称", + "folderNameHint": "按 Enter 确认,Escape 取消", + "emptyFolderName": "请输入文件夹名称", + "invalidFolderName": "文件夹名称包含无效字符", + "noDragState": "未找到待处理的拖放操作" + }, + "empty": { + "noFolders": "未找到文件夹", + "dragHint": "拖拽项目到此处以创建文件夹" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "重试" } } -} \ No newline at end of file +} diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 849294ad..e4eb22e7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1,8 +1,11 @@ { "common": { + "cancel": "取消", + "confirm": "確認", "actions": { "save": "儲存", "cancel": "取消", + "confirm": "確認", "delete": "刪除", "move": "移動", "refresh": "重新整理", @@ -361,6 +364,23 @@ "defaultEmbeddingRootHelp": "設定下載、匯入和移動時的預設 Embedding 根目錄", "noDefault": "未設定預設" }, + "extraFolderPaths": { + "title": "額外資料夾路徑", + "help": "在 ComfyUI 的標準路徑之外新增額外的模型資料夾。這些路徑單獨儲存,並與預設資料夾一起掃描。", + "description": "設定額外的資料夾以掃描模型。這些路徑是 LoRA Manager 特有的,將與 ComfyUI 的預設路徑合併。", + "modelTypes": { + "lora": "LoRA 路徑", + "checkpoint": "Checkpoint 路徑", + "unet": "Diffusion 模型路徑", + "embedding": "Embedding 路徑" + }, + "pathPlaceholder": "/額外/模型/路徑", + "saveSuccess": "額外資料夾路徑已更新。", + "saveError": "更新額外資料夾路徑失敗:{message}", + "validation": { + "duplicatePath": "此路徑已設定" + } + }, "priorityTags": { "title": "優先標籤", "description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))", @@ -485,23 +505,6 @@ "proxyPassword": "密碼(選填)", "proxyPasswordPlaceholder": "password", "proxyPasswordHelp": "代理驗證所需的密碼(如有需要)" - }, - "extraFolderPaths": { - "title": "額外資料夾路徑", - "help": "在 ComfyUI 的標準路徑之外新增額外的模型資料夾。這些路徑單獨儲存,並與預設資料夾一起掃描。", - "description": "設定額外的資料夾以掃描模型。這些路徑是 LoRA Manager 特有的,將與 ComfyUI 的預設路徑合併。", - "modelTypes": { - "lora": "LoRA 路徑", - "checkpoint": "Checkpoint 路徑", - "unet": "Diffusion 模型路徑", - "embedding": "Embedding 路徑" - }, - "pathPlaceholder": "/額外/模型/路徑", - "saveSuccess": "額外資料夾路徑已更新。", - "saveError": "更新額外資料夾路徑失敗:{message}", - "validation": { - "duplicatePath": "此路徑已設定" - } } }, "loras": { @@ -750,7 +753,17 @@ "collapseAllDisabled": "列表檢視下不可用", "dragDrop": { "unableToResolveRoot": "無法確定移動的目標路徑。", - "moveUnsupported": "Move is not supported for this item." + "moveUnsupported": "Move is not supported for this item.", + "createFolderHint": "放開以建立新資料夾", + "newFolderName": "新資料夾名稱", + "folderNameHint": "按 Enter 確認,Escape 取消", + "emptyFolderName": "請輸入資料夾名稱", + "invalidFolderName": "資料夾名稱包含無效字元", + "noDragState": "未找到待處理的拖放操作" + }, + "empty": { + "noFolders": "未找到資料夾", + "dragHint": "將項目拖到此處以建立資料夾" } }, "statistics": { @@ -1658,4 +1671,4 @@ "retry": "重試" } } -} \ No newline at end of file +} diff --git a/static/css/components/sidebar.css b/static/css/components/sidebar.css index 340950e9..24d26c3f 100644 --- a/static/css/components/sidebar.css +++ b/static/css/components/sidebar.css @@ -573,3 +573,171 @@ .sidebar-tree-container::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* ===== Drag and Drop - Create Folder Zone ===== */ + +/* Empty state drag hint */ +.sidebar-empty-hint { + margin-top: 12px; + font-size: 0.8em; + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px; + border-radius: var(--border-radius-xs); + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.05); + border: 1px dashed oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.2); +} + +.sidebar-empty-hint i { + font-size: 0.9em; + opacity: 0.8; + margin: 0; + display: inline; +} + +/* Create folder drop zone */ +.sidebar-create-folder-zone { + position: absolute; + bottom: 16px; + left: 16px; + right: 16px; + padding: 16px; + border: 2px dashed oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.4); + border-radius: var(--border-radius-xs); + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.08); + opacity: 0; + transform: translateY(10px); + transition: all 0.2s ease; + pointer-events: none; + z-index: 10; +} + +.sidebar-create-folder-zone.active { + opacity: 1; + transform: translateY(0); +} + +.sidebar-create-folder-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: var(--lora-accent); + font-size: 0.85em; + text-align: center; +} + +.sidebar-create-folder-content i { + font-size: 1.5em; + opacity: 0.8; +} + +/* Create folder input container */ +.sidebar-create-folder-input-container { + position: absolute; + bottom: 16px; + left: 16px; + right: 16px; + padding: 12px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); + z-index: 20; + animation: slideUp 0.2s ease; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.sidebar-create-folder-input-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.sidebar-create-folder-input-wrapper > i { + color: var(--lora-accent); + font-size: 1em; +} + +.sidebar-create-folder-input { + flex: 1; + padding: 6px 10px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.85em; + outline: none; + transition: all 0.2s ease; +} + +.sidebar-create-folder-input:focus { + border-color: var(--lora-accent); + box-shadow: 0 0 0 2px oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15); +} + +.sidebar-create-folder-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: var(--border-radius-xs); + cursor: pointer; + transition: all 0.2s ease; + background: transparent; + color: var(--text-muted); +} + +.sidebar-create-folder-btn:hover { + background: var(--lora-surface); + color: var(--text-color); +} + +.sidebar-create-folder-confirm:hover { + background: oklch(from var(--success-color) l c h / 0.15); + color: var(--success-color); +} + +.sidebar-create-folder-cancel:hover { + background: oklch(from var(--error-color) l c h / 0.15); + color: var(--error-color); +} + +.sidebar-create-folder-hint { + margin-top: 6px; + font-size: 0.75em; + color: var(--text-muted); + text-align: center; + opacity: 0.8; +} + +/* Dragging state for sidebar */ +.folder-sidebar.dragging-active { + border-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.5); + box-shadow: 0 0 0 3px oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1), + 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.folder-sidebar.dragging-active .sidebar-tree-container { + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.02); +} + +/* Tree container positioning for create folder elements */ +.sidebar-tree-container { + position: relative; +} diff --git a/static/js/components/SidebarManager.js b/static/js/components/SidebarManager.js index f90e4806..6c53dd28 100644 --- a/static/js/components/SidebarManager.js +++ b/static/js/components/SidebarManager.js @@ -29,11 +29,14 @@ export class SidebarManager { this.draggedRootPath = null; this.draggedFromBulk = false; this.dragHandlersInitialized = false; + this.sidebarDragHandlersInitialized = false; this.folderTreeElement = null; this.currentDropTarget = null; this.lastPageControls = null; this.isDisabledBySetting = false; this.initializationPromise = null; + this.isCreatingFolder = false; + this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态 // Bind methods this.handleTreeClick = this.handleTreeClick.bind(this); @@ -56,6 +59,12 @@ export class SidebarManager { this.handleFolderDragOver = this.handleFolderDragOver.bind(this); this.handleFolderDragLeave = this.handleFolderDragLeave.bind(this); this.handleFolderDrop = this.handleFolderDrop.bind(this); + this.handleSidebarDragEnter = this.handleSidebarDragEnter.bind(this); + this.handleSidebarDragOver = this.handleSidebarDragOver.bind(this); + this.handleSidebarDragLeave = this.handleSidebarDragLeave.bind(this); + this.handleSidebarDrop = this.handleSidebarDrop.bind(this); + this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this); + this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this); } setHostPageControls(pageControls) { @@ -118,19 +127,18 @@ export class SidebarManager { this.removeEventHandlers(); this.clearAllDropHighlights(); - if (this.dragHandlersInitialized) { - document.removeEventListener('dragstart', this.handleCardDragStart); - document.removeEventListener('dragend', this.handleCardDragEnd); - this.dragHandlersInitialized = false; - } - if (this.folderTreeElement) { - this.folderTreeElement.removeEventListener('dragenter', this.handleFolderDragEnter); - this.folderTreeElement.removeEventListener('dragover', this.handleFolderDragOver); - this.folderTreeElement.removeEventListener('dragleave', this.handleFolderDragLeave); - this.folderTreeElement.removeEventListener('drop', this.handleFolderDrop); - this.folderTreeElement = null; - } this.resetDragState(); + this.hideCreateFolderInput(); + + // Cleanup sidebar drag handlers + const sidebar = document.getElementById('folderSidebar'); + if (sidebar && this.sidebarDragHandlersInitialized) { + sidebar.removeEventListener('dragenter', this.handleSidebarDragEnter); + sidebar.removeEventListener('dragover', this.handleSidebarDragOver); + sidebar.removeEventListener('dragleave', this.handleSidebarDragLeave); + sidebar.removeEventListener('drop', this.handleSidebarDrop); + this.sidebarDragHandlersInitialized = false; + } // Reset state this.pageControls = null; @@ -233,6 +241,16 @@ export class SidebarManager { this.folderTreeElement = folderTree; } + + // Add sidebar-level drag handlers for creating new folders + const sidebar = document.getElementById('folderSidebar'); + if (sidebar && !this.sidebarDragHandlersInitialized) { + sidebar.addEventListener('dragenter', this.handleSidebarDragEnter); + sidebar.addEventListener('dragover', this.handleSidebarDragOver); + sidebar.addEventListener('dragleave', this.handleSidebarDragLeave); + sidebar.addEventListener('drop', this.handleSidebarDrop); + this.sidebarDragHandlersInitialized = true; + } } handleCardDragStart(event) { @@ -271,6 +289,12 @@ export class SidebarManager { } card.classList.add('dragging'); + + // Add dragging state to sidebar for visual feedback + const sidebar = document.getElementById('folderSidebar'); + if (sidebar) { + sidebar.classList.add('dragging-active'); + } } handleCardDragEnd(event) { @@ -278,6 +302,13 @@ export class SidebarManager { if (card) { card.classList.remove('dragging'); } + + // Remove dragging state from sidebar + const sidebar = document.getElementById('folderSidebar'); + if (sidebar) { + sidebar.classList.remove('dragging-active'); + } + this.clearAllDropHighlights(); this.resetDragState(); } @@ -417,7 +448,12 @@ export class SidebarManager { } async performDragMove(targetRelativePath) { + console.log('[SidebarManager] performDragMove called with targetRelativePath:', targetRelativePath); + console.log('[SidebarManager] draggedFilePaths:', this.draggedFilePaths); + console.log('[SidebarManager] draggedRootPath:', this.draggedRootPath); + if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) { + console.log('[SidebarManager] performDragMove returning false - no draggedFilePaths'); return false; } @@ -428,12 +464,15 @@ export class SidebarManager { } if (this.apiClient?.apiConfig?.config?.supportsMove === false) { + console.log('[SidebarManager] performDragMove returning false - supportsMove is false'); showToast('toast.models.moveFailed', { message: translate('sidebar.dragDrop.moveUnsupported', {}, 'Move not supported for this page') }, 'error'); return false; } const rootPath = this.draggedRootPath ? this.draggedRootPath.replace(/\\/g, '/') : ''; + console.log('[SidebarManager] rootPath:', rootPath); if (!rootPath) { + console.log('[SidebarManager] performDragMove returning false - no rootPath'); showToast( 'toast.models.moveFailed', { message: translate('sidebar.dragDrop.unableToResolveRoot', {}, 'Unable to determine destination path for move.') }, @@ -446,15 +485,19 @@ export class SidebarManager { const useBulkMove = this.draggedFromBulk || this.draggedFilePaths.length > 1; try { + console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove); if (useBulkMove) { await this.apiClient.moveBulkModels(this.draggedFilePaths, destination); } else { await this.apiClient.moveSingleModel(this.draggedFilePaths[0], destination); } + console.log('[SidebarManager] apiClient.move successful'); if (this.pageControls && typeof this.pageControls.resetAndReload === 'function') { + console.log('[SidebarManager] calling resetAndReload'); await this.pageControls.resetAndReload(true); } else { + console.log('[SidebarManager] calling refresh'); await this.refresh(); } @@ -462,10 +505,12 @@ export class SidebarManager { bulkManager.toggleBulkMode(); } + console.log('[SidebarManager] performDragMove returning true'); return true; } catch (error) { - console.error('Error moving model(s) via drag-and-drop:', error); + console.error('[SidebarManager] Error moving model(s) via drag-and-drop:', error); showToast('toast.models.moveFailed', { message: error.message || 'Unknown error' }, 'error'); + console.log('[SidebarManager] performDragMove returning false due to error'); return false; } } @@ -476,6 +521,365 @@ export class SidebarManager { this.draggedFromBulk = false; } + // Version of performDragMove that accepts state as parameters (for create folder submit) + async performDragMoveWithState(targetRelativePath, draggedFilePaths, draggedRootPath, draggedFromBulk) { + console.log('[SidebarManager] performDragMoveWithState called with:', { targetRelativePath, draggedFilePaths, draggedRootPath, draggedFromBulk }); + + if (!draggedFilePaths || draggedFilePaths.length === 0) { + console.log('[SidebarManager] performDragMoveWithState returning false - no draggedFilePaths'); + return false; + } + + if (!this.apiClient) { + this.apiClient = this.pageControls?.getSidebarApiClient?.() + || this.pageControls?.sidebarApiClient + || getModelApiClient(); + } + + if (this.apiClient?.apiConfig?.config?.supportsMove === false) { + console.log('[SidebarManager] performDragMoveWithState returning false - supportsMove is false'); + showToast('toast.models.moveFailed', { message: translate('sidebar.dragDrop.moveUnsupported', {}, 'Move not supported for this page') }, 'error'); + return false; + } + + const rootPath = draggedRootPath ? draggedRootPath.replace(/\\/g, '/') : ''; + console.log('[SidebarManager] rootPath:', rootPath); + if (!rootPath) { + console.log('[SidebarManager] performDragMoveWithState returning false - no rootPath'); + showToast( + 'toast.models.moveFailed', + { message: translate('sidebar.dragDrop.unableToResolveRoot', {}, 'Unable to determine destination path for move.') }, + 'error' + ); + return false; + } + + const destination = this.combineRootAndRelativePath(rootPath, targetRelativePath); + const useBulkMove = draggedFromBulk || draggedFilePaths.length > 1; + + try { + console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove); + if (useBulkMove) { + await this.apiClient.moveBulkModels(draggedFilePaths, destination); + } else { + await this.apiClient.moveSingleModel(draggedFilePaths[0], destination); + } + console.log('[SidebarManager] apiClient.move successful'); + + if (this.pageControls && typeof this.pageControls.resetAndReload === 'function') { + console.log('[SidebarManager] calling resetAndReload'); + await this.pageControls.resetAndReload(true); + } else { + console.log('[SidebarManager] calling refresh'); + await this.refresh(); + } + + if (draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') { + bulkManager.toggleBulkMode(); + } + + console.log('[SidebarManager] performDragMoveWithState returning true'); + return true; + } catch (error) { + console.error('[SidebarManager] Error moving model(s) via drag-and-drop:', error); + showToast('toast.models.moveFailed', { message: error.message || 'Unknown error' }, 'error'); + console.log('[SidebarManager] performDragMoveWithState returning false due to error'); + return false; + } + } + + // ===== Sidebar-level drag handlers for creating new folders ===== + + handleSidebarDragEnter(event) { + if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return; + + const sidebar = document.getElementById('folderSidebar'); + if (!sidebar) return; + + // Only show create folder zone if not hovering over an existing folder + const folderElement = this.getFolderElementFromEvent(event); + if (folderElement) { + this.hideCreateFolderZone(); + return; + } + + // Check if drag is within the sidebar tree container area + const treeContainer = document.querySelector('.sidebar-tree-container'); + if (treeContainer && treeContainer.contains(event.target)) { + event.preventDefault(); + this.showCreateFolderZone(); + } + } + + handleSidebarDragOver(event) { + if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return; + + const folderElement = this.getFolderElementFromEvent(event); + if (folderElement) { + this.hideCreateFolderZone(); + return; + } + + const treeContainer = document.querySelector('.sidebar-tree-container'); + if (treeContainer && treeContainer.contains(event.target)) { + event.preventDefault(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + } + } + + handleSidebarDragLeave(event) { + if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return; + + const sidebar = document.getElementById('folderSidebar'); + if (!sidebar) return; + + const relatedTarget = event.relatedTarget instanceof Element ? event.relatedTarget : null; + + // Only hide if leaving the sidebar entirely + if (!relatedTarget || !sidebar.contains(relatedTarget)) { + this.hideCreateFolderZone(); + } + } + + async handleSidebarDrop(event) { + if (!this.draggedFilePaths || this.draggedFilePaths.length === 0) return; + + const folderElement = this.getFolderElementFromEvent(event); + if (folderElement) { + // Let the folder drop handler take over + return; + } + + const treeContainer = document.querySelector('.sidebar-tree-container'); + if (!treeContainer || !treeContainer.contains(event.target)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + // Show create folder input + this.showCreateFolderInput(); + } + + showCreateFolderZone() { + if (this.isCreatingFolder) return; + + const treeContainer = document.querySelector('.sidebar-tree-container'); + if (!treeContainer) return; + + let zone = document.getElementById('sidebarCreateFolderZone'); + if (!zone) { + zone = document.createElement('div'); + zone.id = 'sidebarCreateFolderZone'; + zone.className = 'sidebar-create-folder-zone'; + zone.innerHTML = ` + + `; + treeContainer.appendChild(zone); + } + + zone.classList.add('active'); + } + + hideCreateFolderZone() { + const zone = document.getElementById('sidebarCreateFolderZone'); + if (zone) { + zone.classList.remove('active'); + } + } + + showCreateFolderInput() { + console.log('[SidebarManager] showCreateFolderInput called'); + this.isCreatingFolder = true; + + // 立即保存拖拽状态,防止后续事件(如blur)清空状态 + this._pendingDragState = { + filePaths: this.draggedFilePaths ? [...this.draggedFilePaths] : null, + rootPath: this.draggedRootPath, + fromBulk: this.draggedFromBulk + }; + console.log('[SidebarManager] saved pending drag state:', this._pendingDragState); + + this.hideCreateFolderZone(); + + const treeContainer = document.querySelector('.sidebar-tree-container'); + if (!treeContainer) return; + + // Remove existing input if any + this.hideCreateFolderInput(); + + const inputContainer = document.createElement('div'); + inputContainer.id = 'sidebarCreateFolderInput'; + inputContainer.className = 'sidebar-create-folder-input-container'; + inputContainer.innerHTML = ` + + + `; + + treeContainer.appendChild(inputContainer); + + // Focus input + const input = inputContainer.querySelector('.sidebar-create-folder-input'); + if (input) { + input.focus(); + } + + // Bind events + const confirmBtn = inputContainer.querySelector('.sidebar-create-folder-confirm'); + const cancelBtn = inputContainer.querySelector('.sidebar-create-folder-cancel'); + + // Flag to prevent blur from canceling when clicking buttons + let isButtonClick = false; + + confirmBtn?.addEventListener('mousedown', () => { + isButtonClick = true; + console.log('[SidebarManager] confirmBtn mousedown - isButtonClick set to true'); + }); + cancelBtn?.addEventListener('mousedown', () => { + isButtonClick = true; + console.log('[SidebarManager] cancelBtn mousedown - isButtonClick set to true'); + }); + + confirmBtn?.addEventListener('click', (e) => { + console.log('[SidebarManager] confirmBtn click event triggered'); + this.handleCreateFolderSubmit(); + }); + cancelBtn?.addEventListener('click', () => { + console.log('[SidebarManager] cancelBtn click event triggered'); + this.handleCreateFolderCancel(); + }); + input?.addEventListener('keydown', (e) => { + console.log('[SidebarManager] input keydown:', e.key); + if (e.key === 'Enter') { + console.log('[SidebarManager] Enter pressed, calling handleCreateFolderSubmit'); + this.handleCreateFolderSubmit(); + } else if (e.key === 'Escape') { + console.log('[SidebarManager] Escape pressed, calling handleCreateFolderCancel'); + this.handleCreateFolderCancel(); + } + }); + input?.addEventListener('blur', () => { + console.log('[SidebarManager] input blur event - isButtonClick:', isButtonClick); + // Delay to allow button clicks to process first + setTimeout(() => { + console.log('[SidebarManager] blur timeout - isButtonClick:', isButtonClick, 'activeElement:', document.activeElement?.className); + if (!isButtonClick && document.activeElement !== confirmBtn && document.activeElement !== cancelBtn) { + console.log('[SidebarManager] blur timeout - calling handleCreateFolderCancel'); + this.handleCreateFolderCancel(); + } else { + console.log('[SidebarManager] blur timeout - NOT canceling (button click detected)'); + } + isButtonClick = false; + }, 200); + }); + } + + hideCreateFolderInput() { + console.log('[SidebarManager] hideCreateFolderInput called'); + const inputContainer = document.getElementById('sidebarCreateFolderInput'); + console.log('[SidebarManager] inputContainer:', inputContainer); + if (inputContainer) { + inputContainer.remove(); + console.log('[SidebarManager] inputContainer removed'); + } + this.isCreatingFolder = false; + console.log('[SidebarManager] isCreatingFolder set to false'); + } + + async handleCreateFolderSubmit() { + console.log('[SidebarManager] handleCreateFolderSubmit called'); + const input = document.querySelector('#sidebarCreateFolderInput .sidebar-create-folder-input'); + console.log('[SidebarManager] input element:', input); + if (!input) { + console.log('[SidebarManager] input not found, returning'); + return; + } + + const folderName = input.value.trim(); + console.log('[SidebarManager] folderName:', folderName); + if (!folderName) { + showToast('sidebar.dragDrop.emptyFolderName', {}, 'warning'); + return; + } + + // Validate folder name (no slashes, no special chars) + if (/[\\/:*?"<>|]/.test(folderName)) { + showToast('sidebar.dragDrop.invalidFolderName', {}, 'error'); + return; + } + + // Build target path - use selected path as parent, or root if none selected + const parentPath = this.selectedPath || ''; + const targetRelativePath = parentPath ? `${parentPath}/${folderName}` : folderName; + console.log('[SidebarManager] targetRelativePath:', targetRelativePath); + + // 使用 showCreateFolderInput 时保存的拖拽状态 + const pendingState = this._pendingDragState; + console.log('[SidebarManager] using pending drag state:', pendingState); + + if (!pendingState || !pendingState.filePaths || pendingState.filePaths.length === 0) { + console.log('[SidebarManager] no pending drag state found, cannot proceed'); + showToast('sidebar.dragDrop.noDragState', {}, 'error'); + this.hideCreateFolderInput(); + return; + } + + this.hideCreateFolderInput(); + + // Perform the move with saved state + console.log('[SidebarManager] calling performDragMove with pending state'); + const success = await this.performDragMoveWithState(targetRelativePath, pendingState.filePaths, pendingState.rootPath, pendingState.fromBulk); + console.log('[SidebarManager] performDragMove result:', success); + + if (success) { + // Expand the parent folder to show the new folder + if (parentPath) { + this.expandedNodes.add(parentPath); + this.saveExpandedState(); + } + // Refresh the tree to show the newly created folder + // restoreSelectedFolder() inside refresh() will maintain the current active folder + await this.refresh(); + } + + // 清理待处理的拖拽状态 + this._pendingDragState = null; + this.resetDragState(); + this.clearAllDropHighlights(); + } + + handleCreateFolderCancel() { + this.hideCreateFolderInput(); + // 清理待处理的拖拽状态 + this._pendingDragState = null; + this.resetDragState(); + this.clearAllDropHighlights(); + } + + saveSelectedFolder() { + setStorageItem(`${this.pageType}_activeFolder`, this.selectedPath); + } + clearAllDropHighlights() { const highlighted = document.querySelectorAll('.sidebar-tree-node-content.drop-target, .sidebar-node-content.drop-target'); highlighted.forEach((element) => element.classList.remove('drop-target')); @@ -917,7 +1321,11 @@ export class SidebarManager { folderTree.innerHTML = ` `; } diff --git a/static/js/managers/MoveManager.js b/static/js/managers/MoveManager.js index 58c89a41..4ffad779 100644 --- a/static/js/managers/MoveManager.js +++ b/static/js/managers/MoveManager.js @@ -286,6 +286,9 @@ class MoveManager { if (recursive) { // Visible if it's in activeFolder or any subfolder + // Special case for root: if activeFolder is empty, everything is visible in recursive mode + if (normalizedActive === '') return true; + return normalizedRelative === normalizedActive || normalizedRelative.startsWith(normalizedActive + '/'); } else { @@ -315,81 +318,31 @@ class MoveManager { try { if (this.bulkFilePaths) { // Bulk move mode - const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath); - - // Update virtual scroller visibility/metadata - const pageState = getCurrentPageState(); - if (state.virtualScroller) { - results.forEach(result => { - if (result.success) { - // Deselect moving item - bulkManager.deselectItem(result.original_file_path); - - const newRelativeFolder = this._getRelativeFolder(result.new_file_path); - const isVisible = this._isModelVisible(newRelativeFolder, pageState); - - if (!isVisible) { - state.virtualScroller.removeItemByFilePath(result.original_file_path); - } else { - const newFileNameWithExt = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1); - const baseFileName = newFileNameWithExt.substring(0, newFileNameWithExt.lastIndexOf('.')); - - const updateData = { - file_path: result.new_file_path, - file_name: baseFileName, - folder: newRelativeFolder - }; - - // Only update sub_type if it's present in the cache_entry - if (result.cache_entry && result.cache_entry.sub_type) { - updateData.sub_type = result.cache_entry.sub_type; - } - - state.virtualScroller.updateSingleItem(result.original_file_path, updateData); - } - } - }); - } + await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath); + + // Deselect moving items + this.bulkFilePaths.forEach(path => bulkManager.deselectItem(path)); } else { // Single move mode - const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath); - - const pageState = getCurrentPageState(); - if (result && result.new_file_path && state.virtualScroller) { - // Deselect moving item - bulkManager.deselectItem(this.currentFilePath); - - const newRelativeFolder = this._getRelativeFolder(result.new_file_path); - const isVisible = this._isModelVisible(newRelativeFolder, pageState); - - if (!isVisible) { - state.virtualScroller.removeItemByFilePath(this.currentFilePath); - } else { - const newFileNameWithExt = result.new_file_path.substring(result.new_file_path.lastIndexOf('/') + 1); - const baseFileName = newFileNameWithExt.substring(0, newFileNameWithExt.lastIndexOf('.')); - - const updateData = { - file_path: result.new_file_path, - file_name: baseFileName, - folder: newRelativeFolder - }; - - // Only update sub_type if it's present in the cache_entry - if (result.cache_entry && result.cache_entry.sub_type) { - updateData.sub_type = result.cache_entry.sub_type; - } - - state.virtualScroller.updateSingleItem(this.currentFilePath, updateData); - } - } + await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath); + + // Deselect moving item + bulkManager.deselectItem(this.currentFilePath); } - // Refresh folder tags after successful move - sidebarManager.refresh(); + // Refresh UI by reloading the current page, same as drag-and-drop behavior + // This ensures all metadata (like preview URLs) are correctly formatted by the backend + if (sidebarManager.pageControls && typeof sidebarManager.pageControls.resetAndReload === 'function') { + await sidebarManager.pageControls.resetAndReload(true); + } else if (sidebarManager.lastPageControls && typeof sidebarManager.lastPageControls.resetAndReload === 'function') { + await sidebarManager.lastPageControls.resetAndReload(true); + } + + // Refresh folder tree in sidebar + await sidebarManager.refresh(); modalManager.closeModal('moveModal'); - } catch (error) { console.error('Error moving model(s):', error); showToast('toast.models.moveFailed', { message: error.message }, 'error');