mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(settings): editable model library paths for standalone mode
Standalone users previously had to hand-edit settings.json to configure primary folder_paths. Add a standalone-only Model Paths section to the settings modal: - Backend exposes standalone_mode, folder_paths (with template placeholder values filtered out) and a data-driven folder_path_schema derived from OTHER_MODEL_FOLDER_SUBTYPES via GET /api/lm/settings - The new section renders multi-path editors per model type from the schema, with inline enable_other_models / sub-type controls so other model types are configured without leaving the tab - Persistent restart-required cues after a save: nav dot, inline notice and a global banner (unique id per change so dismissals don't mute future reminders) - The missing-model-paths startup banner and the Other Models no-paths empty state now deep-link into the new section instead of pointing at settings.json
This commit is contained in:
@@ -192,6 +192,15 @@ The system runs in two modes:
|
||||
- Auto-saves paths to `settings.json` in ComfyUI mode
|
||||
- `settings.json.example` is intentionally minimal (see Important Notes); all
|
||||
other defaults live in `DEFAULT_SETTINGS` (`py/services/settings_manager.py`)
|
||||
- **`folder_paths` vs `extra_folder_paths` — different purposes, do not conflate:**
|
||||
- `folder_paths` (primary model roots): in ComfyUI plugin mode these come
|
||||
from the ComfyUI host; in standalone mode they are the ONLY source of
|
||||
model library paths and are currently edited by hand in `settings.json`.
|
||||
- `extra_folder_paths` is a **ComfyUI-plugin-mode feature**: paths visible
|
||||
ONLY to LoRA Manager, not to ComfyUI. Its motivation is that a very large
|
||||
model library slows ComfyUI itself down, while LoRA Manager handles large
|
||||
libraries without performance issues — so users keep ComfyUI's library
|
||||
small and add the bulk via `extra_folder_paths`.
|
||||
|
||||
### Frontend UI Architecture
|
||||
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "Allgemein",
|
||||
"interface": "Oberfläche",
|
||||
"library": "Bibliothek"
|
||||
"library": "Bibliothek",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Einstellungen durchsuchen...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "Dieser Pfad wird bereits für einen anderen Modelltyp verwendet. Bitte verwenden Sie separate Ordner für Checkpoints und Diffusionsmodelle."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"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))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "Keine Ordner für weitere Modelle gefunden",
|
||||
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber es wurden keine Ordner für weitere Modelle gefunden. Fügen Sie die benötigten Ordnerschlüssel zum Abschnitt folder_paths Ihrer settings.json hinzu und starten Sie LoRA Manager neu.",
|
||||
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die entsprechenden Modellordner zu Ihren ComfyUI-Modellpfaden hinzu und laden Sie diese Seite neu.",
|
||||
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
|
||||
"openSettings": "Einstellungen öffnen",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "Einstellungsordner öffnen"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "General",
|
||||
"interface": "Interface",
|
||||
"library": "Library"
|
||||
"library": "Library",
|
||||
"modelPaths": "Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search settings...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "This path is also used for a different model type. Use separate folders for checkpoints and diffusion models."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "Model Library Paths",
|
||||
"description": "Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "Requires restart to take effect",
|
||||
"coreTypes": "Core Model Types",
|
||||
"otherTypes": "Other Model Types",
|
||||
"otherTypesDisabledHint": "No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "LoRA Paths",
|
||||
"checkpoints": "Checkpoint Paths",
|
||||
"unet": "Diffusion Model Paths",
|
||||
"embeddings": "Embedding Paths",
|
||||
"vae": "VAE Paths",
|
||||
"upscale_models": "Upscaler Paths",
|
||||
"text_encoders": "Text Encoder Paths",
|
||||
"clip": "CLIP Paths (legacy)",
|
||||
"clip_vision": "CLIP Vision Paths",
|
||||
"controlnet": "ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type (e.g., character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "No other-model folders found",
|
||||
"descriptionStandalone": "Other Models management is on, but no other-model folders were found. Add the folder keys you need to the folder_paths section of your settings.json, then restart LoRA Manager.",
|
||||
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.",
|
||||
"descriptionStandalone": "Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "Other Models management is on, but none of the configured model folders exist on disk. Add the matching model folders to your ComfyUI model paths, then reload this page.",
|
||||
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
|
||||
"openSettings": "Open Settings",
|
||||
"openModelPaths": "Configure Model Folders",
|
||||
"openSettingsFolder": "Open Settings Folder"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "General",
|
||||
"interface": "Interfaz",
|
||||
"library": "Biblioteca"
|
||||
"library": "Biblioteca",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Buscar ajustes...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "Esta ruta ya se usa para otro tipo de modelo. Use carpetas separadas para checkpoints y modelos de difusión."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"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))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "No se encontraron carpetas de otros modelos",
|
||||
"descriptionStandalone": "La gestión de otros modelos está activada, pero no se encontraron carpetas de otros modelos. Añade las claves de carpeta que necesites a la sección folder_paths de tu settings.json y reinicia LoRA Manager.",
|
||||
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las carpetas de modelos correspondientes a tus rutas de modelos de ComfyUI y recarga esta página.",
|
||||
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
|
||||
"openSettings": "Abrir configuración",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "Abrir carpeta de ajustes"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "Général",
|
||||
"interface": "Interface",
|
||||
"library": "Bibliothèque"
|
||||
"library": "Bibliothèque",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Rechercher dans les paramètres...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "Ce chemin est déjà utilisé pour un autre type de modèle. Utilisez des dossiers séparés pour les checkpoints et les modèles de diffusion."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Tags prioritaires",
|
||||
"description": "Personnalisez l'ordre de priorité des tags pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "Aucun dossier d’autres modèles trouvé",
|
||||
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun dossier d’autres modèles n’a été trouvé. Ajoutez les clés de dossiers dont vous avez besoin à la section folder_paths de votre settings.json, puis redémarrez LoRA Manager.",
|
||||
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.",
|
||||
"hintComfyUI": "Les autres modèles sont lus depuis les dossiers vae, upscale_models, text_encoders, clip_vision et controlnet de ComfyUI.",
|
||||
"openSettings": "Ouvrir les paramètres",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "Ouvrir le dossier des paramètres"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "כללי",
|
||||
"interface": "ממשק",
|
||||
"library": "ספרייה"
|
||||
"library": "ספרייה",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "חיפוש בהגדרות...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "הנתיב הזה כבר נמצא בשימוש עבור סוג מודל אחר. יש להשתמש בתיקיות נפרדות עבור checkpoints ומודלי דיפוזיה."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "תגיות עדיפות",
|
||||
"description": "התאם את סדר העדיפות של התגיות עבור כל סוג מודל (לדוגמה: character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "לא נמצאו תיקיות של מודלים אחרים",
|
||||
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך לא נמצאו תיקיות של מודלים אחרים. הוסף את מפתחות התיקיות הדרושים למקטע folder_paths ב-settings.json והפעל מחדש את LoRA Manager.",
|
||||
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
|
||||
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
|
||||
"openSettings": "פתח הגדרות",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "פתח תיקיית הגדרות"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "一般",
|
||||
"interface": "インターフェース",
|
||||
"library": "ライブラリ"
|
||||
"library": "ライブラリ",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "設定を検索...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "このパスは別のモデルタイプですでに使用されています。Checkpoints と diffusion models には別々のフォルダを使用してください。"
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先タグ",
|
||||
"description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "その他のモデルのフォルダーが見つかりません",
|
||||
"descriptionStandalone": "その他のモデル管理はオンですが、その他のモデルのフォルダーが見つかりません。必要なフォルダーキーをsettings.jsonのfolder_pathsセクションに追加し、LoRA Managerを再起動してください。",
|
||||
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
|
||||
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
|
||||
"openSettings": "設定を開く",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "設定フォルダーを開く"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "일반",
|
||||
"interface": "인터페이스",
|
||||
"library": "라이브러리"
|
||||
"library": "라이브러리",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "설정 검색...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "이 경로는 다른 모델 유형에 이미 사용 중입니다. checkpoints와 diffusion models에 별도의 폴더를 사용하세요."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "우선순위 태그",
|
||||
"description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "기타 모델 폴더를 찾을 수 없습니다",
|
||||
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 기타 모델 폴더를 찾을 수 없습니다. 필요한 폴더 키를 settings.json의 folder_paths 섹션에 추가한 뒤 LoRA Manager를 재시작하세요.",
|
||||
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
|
||||
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
|
||||
"openSettings": "설정 열기",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "설정 폴더 열기"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "Общее",
|
||||
"interface": "Интерфейс",
|
||||
"library": "Библиотека"
|
||||
"library": "Библиотека",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Поиск в настройках...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "Этот путь уже используется для другого типа модели. Используйте отдельные папки для checkpoints и diffusion models."
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Приоритетные теги",
|
||||
"description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "Папки других моделей не найдены",
|
||||
"descriptionStandalone": "Управление другими моделями включено, но папки других моделей не найдены. Добавьте нужные ключи папок в раздел folder_paths файла settings.json и перезапустите LoRA Manager.",
|
||||
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
|
||||
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
|
||||
"openSettings": "Открыть настройки",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "Открыть папку настроек"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "通用",
|
||||
"interface": "界面",
|
||||
"library": "库"
|
||||
"library": "库",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索设置...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "此路径已被用于另一种模型类型。请为 checkpoints 和 diffusion models 使用不同的文件夹。"
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "优先标签",
|
||||
"description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "未找到其他模型文件夹",
|
||||
"descriptionStandalone": "其他模型管理已开启,但未找到其他模型文件夹。请将你需要的文件夹键添加到 settings.json 的 folder_paths 部分,然后重启 LoRA Manager。",
|
||||
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
|
||||
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
|
||||
"openSettings": "打开设置",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "打开设置文件夹"
|
||||
}
|
||||
},
|
||||
|
||||
+29
-3
@@ -382,7 +382,8 @@
|
||||
"nav": {
|
||||
"general": "通用",
|
||||
"interface": "介面",
|
||||
"library": "模型庫"
|
||||
"library": "模型庫",
|
||||
"modelPaths": "[TODO: Translate] Model Paths"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜尋設定...",
|
||||
@@ -583,6 +584,30 @@
|
||||
"checkpointUnetOverlapInline": "此路徑已被用於另一種模型類型。請為 checkpoints 和 diffusion models 使用不同的資料夾。"
|
||||
}
|
||||
},
|
||||
"modelPaths": {
|
||||
"title": "[TODO: Translate] Model Library Paths",
|
||||
"description": "[TODO: Translate] Root folders LoRA Manager scans for your models. These are the primary model locations read from settings.json in standalone mode.",
|
||||
"restartRequired": "[TODO: Translate] Requires restart to take effect",
|
||||
"coreTypes": "[TODO: Translate] Core Model Types",
|
||||
"otherTypes": "[TODO: Translate] Other Model Types",
|
||||
"otherTypesDisabledHint": "[TODO: Translate] No other model types are enabled. Turn on the types you need above to configure their folders.",
|
||||
"saveSuccessRestart": "[TODO: Translate] Model library paths updated. Restart required to apply changes.",
|
||||
"pendingRestartNotice": "[TODO: Translate] Path changes saved. Restart LoRA Manager for them to take effect.",
|
||||
"pendingRestartBannerTitle": "[TODO: Translate] Restart required to apply path changes",
|
||||
"pendingRestartBannerMessage": "[TODO: Translate] Model library paths were updated. Restart the LoRA Manager server to scan the new folders.",
|
||||
"folderKeys": {
|
||||
"loras": "[TODO: Translate] LoRA Paths",
|
||||
"checkpoints": "[TODO: Translate] Checkpoint Paths",
|
||||
"unet": "[TODO: Translate] Diffusion Model Paths",
|
||||
"embeddings": "[TODO: Translate] Embedding Paths",
|
||||
"vae": "[TODO: Translate] VAE Paths",
|
||||
"upscale_models": "[TODO: Translate] Upscaler Paths",
|
||||
"text_encoders": "[TODO: Translate] Text Encoder Paths",
|
||||
"clip": "[TODO: Translate] CLIP Paths (legacy)",
|
||||
"clip_vision": "[TODO: Translate] CLIP Vision Paths",
|
||||
"controlnet": "[TODO: Translate] ControlNet Paths"
|
||||
}
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先標籤",
|
||||
"description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))",
|
||||
@@ -1241,11 +1266,12 @@
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "找不到其他模型資料夾",
|
||||
"descriptionStandalone": "其他模型管理已開啟,但找不到其他模型資料夾。請將您需要的資料夾鍵加入 settings.json 的 folder_paths 區段,然後重新啟動 LoRA Manager。",
|
||||
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
|
||||
"descriptionStandalone": "[TODO: Translate] Other Models management is on, but no other-model folders were found. Add your model folders under Settings → Model Paths, then restart LoRA Manager.",
|
||||
"hintStandalone": "[TODO: Translate] Only enabled model types are scanned; enable the types you need under Library → Folder Settings.",
|
||||
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
|
||||
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
|
||||
"openSettings": "開啟設定",
|
||||
"openModelPaths": "[TODO: Translate] Configure Model Folders",
|
||||
"openSettingsFolder": "開啟設定資料夾"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ from ...utils.constants import (
|
||||
SUPPORTED_MEDIA_EXTENSIONS,
|
||||
VALID_LORA_TYPES,
|
||||
VALID_OTHER_CIVITAI_TYPES,
|
||||
folder_path_schema,
|
||||
)
|
||||
from .model_source_handlers import ModelSourceHandler
|
||||
from .agent_handlers import AgentHandler
|
||||
@@ -1580,6 +1581,30 @@ class SettingsHandler:
|
||||
availability_error,
|
||||
)
|
||||
response_data["other_models_paths_available"] = None
|
||||
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
||||
response_data["standalone_mode"] = standalone_mode
|
||||
if standalone_mode:
|
||||
# Standalone reads its model roots exclusively from
|
||||
# settings.json, so the Model Paths settings UI needs the
|
||||
# current values plus the editable-key schema. In plugin mode
|
||||
# the paths come from the ComfyUI host and stay hidden.
|
||||
folder_paths = self._settings.get("folder_paths") or {}
|
||||
# A fresh install is seeded from settings.json.example, whose
|
||||
# folder_paths are documentation placeholders — hide them so
|
||||
# the UI starts with empty editors instead of fake paths.
|
||||
get_placeholders = getattr(
|
||||
self._settings, "get_template_folder_path_placeholders", None
|
||||
)
|
||||
placeholders = get_placeholders() if get_placeholders else set()
|
||||
if placeholders:
|
||||
folder_paths = {
|
||||
key: [p for p in paths if p not in placeholders]
|
||||
if isinstance(paths, list)
|
||||
else paths
|
||||
for key, paths in folder_paths.items()
|
||||
}
|
||||
response_data["folder_paths"] = folder_paths
|
||||
response_data["folder_path_schema"] = folder_path_schema()
|
||||
settings_file = getattr(self._settings, "settings_file", None)
|
||||
if settings_file:
|
||||
response_data["settings_file"] = settings_file
|
||||
|
||||
@@ -89,8 +89,8 @@ class OtherRoutes(BaseModelRoutes):
|
||||
"standalone_mode": standalone_mode,
|
||||
}
|
||||
if standalone_mode:
|
||||
# The settings UI cannot edit primary folder_paths, so the empty
|
||||
# state must point at the actual file the user has to edit.
|
||||
# The empty state points at the Model Paths settings section and
|
||||
# shows the settings.json path as a fallback reference.
|
||||
context["settings_file"] = getattr(self._settings, "settings_file", "") or ""
|
||||
return context
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import (
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
@@ -308,6 +309,29 @@ class SettingsManager:
|
||||
|
||||
return payload == template
|
||||
|
||||
def get_template_folder_path_placeholders(self) -> Set[str]:
|
||||
"""Placeholder folder_paths values shipped in settings.json.example.
|
||||
|
||||
A fresh standalone install is seeded from the template, so its
|
||||
documentation-only placeholder paths end up in the live settings
|
||||
file. The Model Paths settings UI hides them; the first real save
|
||||
overwrites them via ``set("folder_paths")``.
|
||||
"""
|
||||
|
||||
template = self._read_template_payload()
|
||||
if not template:
|
||||
return set()
|
||||
|
||||
folder_paths = template.get("folder_paths")
|
||||
if not isinstance(folder_paths, Mapping):
|
||||
return set()
|
||||
|
||||
placeholders: Set[str] = set()
|
||||
for value in folder_paths.values():
|
||||
paths = value if isinstance(value, list) else [value]
|
||||
placeholders.update(p for p in paths if isinstance(p, str) and p)
|
||||
return placeholders
|
||||
|
||||
def _merge_template_with_defaults(
|
||||
self, defaults: Dict[str, Any], template: Mapping[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
@@ -1219,19 +1243,27 @@ class SettingsManager:
|
||||
if self._bootstrap_reason == "missing":
|
||||
message = (
|
||||
"LoRA Manager created a default settings.json because no configuration was found. "
|
||||
"Edit settings.json to add your model directories so library scanning can run."
|
||||
"Open Settings → Model Paths to add your model directories so library scanning can run."
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
"LoRA Manager could not locate any configured model directories. "
|
||||
"Edit settings.json to add your model folders so library scanning can run."
|
||||
"Open Settings → Model Paths to add your model folders so library scanning can run."
|
||||
)
|
||||
self._add_startup_message(
|
||||
code="missing-model-paths",
|
||||
title="Model folders need setup",
|
||||
message=message,
|
||||
severity="warning",
|
||||
actions=self._default_settings_actions(),
|
||||
actions=[
|
||||
{
|
||||
"action": "open-model-paths-settings",
|
||||
"label": "Configure model folders",
|
||||
"type": "primary",
|
||||
"icon": "fas fa-cog",
|
||||
},
|
||||
*self._default_settings_actions(),
|
||||
],
|
||||
dismissible=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -127,6 +127,29 @@ def other_sub_type_folder_keys() -> Dict[str, List[str]]:
|
||||
# Precomputed inverse of OTHER_MODEL_FOLDER_SUBTYPES, keeping the table order.
|
||||
OTHER_SUB_TYPE_FOLDER_KEYS: Dict[str, List[str]] = other_sub_type_folder_keys()
|
||||
|
||||
# Core folder_paths keys every LoRA Manager installation understands.
|
||||
CORE_FOLDER_PATH_KEYS: List[str] = ["loras", "checkpoints", "unet", "embeddings"]
|
||||
|
||||
|
||||
def folder_path_schema() -> List[Dict[str, Any]]:
|
||||
"""Ordered schema describing the editable folder_paths keys.
|
||||
|
||||
Drives the standalone-only Model Paths settings UI: the frontend renders
|
||||
one multi-path editor per entry and resolves labels via the
|
||||
``settings.modelPaths.folderKeys.<key>`` i18n keys, so adding a new model
|
||||
category is a constants + locale change only. ``sub_type`` lets the UI
|
||||
hide editors for other-model categories the user has not enabled.
|
||||
"""
|
||||
schema: List[Dict[str, Any]] = [
|
||||
{"key": key, "category": "core", "sub_type": None}
|
||||
for key in CORE_FOLDER_PATH_KEYS
|
||||
]
|
||||
schema.extend(
|
||||
{"key": folder_key, "category": "other", "sub_type": sub_type}
|
||||
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items()
|
||||
)
|
||||
return schema
|
||||
|
||||
|
||||
def normalize_other_sub_types(value: Any) -> List[str]:
|
||||
"""Normalize a stored/requested enabled-sub_type list.
|
||||
|
||||
@@ -1780,3 +1780,38 @@ input:checked + .toggle-slider:before {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Standalone Model Paths: pending-restart cues */
|
||||
.settings-nav-item.has-pending-restart {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings-nav-item.has-pending-restart::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--lora-warning, #e67e22);
|
||||
}
|
||||
|
||||
.model-paths-restart-notice {
|
||||
display: none;
|
||||
margin-top: 8px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--lora-warning, #e67e22);
|
||||
background: rgba(230, 126, 34, 0.08);
|
||||
color: var(--lora-warning, #e67e22);
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.model-paths-restart-notice.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,16 @@ import { bannerService } from './BannerService.js';
|
||||
|
||||
const VALID_MATURE_BLUR_LEVELS = new Set(['PG13', 'R', 'X', 'XXX']);
|
||||
|
||||
// Other-model sub_type -> i18n label key, mirroring the checkbox list in
|
||||
// templates/components/modals/settings/library.html.
|
||||
const OTHER_SUB_TYPE_LABEL_KEYS = {
|
||||
vae: 'settings.folderSettings.subTypeVae',
|
||||
upscaler: 'settings.folderSettings.subTypeUpscaler',
|
||||
text_encoder: 'settings.folderSettings.subTypeTextEncoder',
|
||||
clip_vision: 'settings.folderSettings.subTypeClipVision',
|
||||
controlnet: 'settings.folderSettings.subTypeControlnet',
|
||||
};
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
@@ -26,6 +36,8 @@ export class SettingsManager {
|
||||
this.availableLibraries = {};
|
||||
this.activeLibrary = '';
|
||||
this.registeredStartupBannerIds = new Set();
|
||||
this.modelPathsSectionInitialized = false;
|
||||
this.modelPathsDirty = false;
|
||||
|
||||
// Add initialization to sync with modal state
|
||||
this.currentPage = document.body.dataset.page || 'loras';
|
||||
@@ -78,6 +90,7 @@ export class SettingsManager {
|
||||
|
||||
await this.applyLanguageSetting();
|
||||
this.applyFrontendSettings();
|
||||
this.setupModelPathsSection();
|
||||
}
|
||||
|
||||
async applyLanguageSetting() {
|
||||
@@ -276,6 +289,10 @@ export class SettingsManager {
|
||||
case 'open-settings-modal':
|
||||
modalManager.showModal('settingsModal');
|
||||
break;
|
||||
case 'open-model-paths-settings':
|
||||
modalManager.showModal('settingsModal');
|
||||
document.querySelector('.settings-nav-item[data-section="modelPaths"]')?.click();
|
||||
break;
|
||||
case 'open-settings-location':
|
||||
this.openSettingsFileLocation();
|
||||
break;
|
||||
@@ -472,8 +489,11 @@ export class SettingsManager {
|
||||
const sectionId = item.dataset.section;
|
||||
if (!sectionId) return;
|
||||
|
||||
// Hide all sections
|
||||
sections.forEach(section => {
|
||||
// Query live instead of using the captured NodeLists: the
|
||||
// standalone Model Paths section is added after this
|
||||
// initializer runs, and a stale snapshot would leave it
|
||||
// active forever.
|
||||
document.querySelectorAll('.settings-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
});
|
||||
|
||||
@@ -484,7 +504,7 @@ export class SettingsManager {
|
||||
}
|
||||
|
||||
// Update active nav state
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
document.querySelectorAll('.settings-nav-item').forEach(nav => nav.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
@@ -1160,6 +1180,9 @@ export class SettingsManager {
|
||||
// Load extra folder paths
|
||||
this.loadExtraFolderPaths();
|
||||
|
||||
// Load standalone model library paths (no-op in plugin mode)
|
||||
this.loadModelPaths();
|
||||
|
||||
// Load language setting
|
||||
const languageSelect = document.getElementById('languageSelect');
|
||||
if (languageSelect) {
|
||||
@@ -1942,6 +1965,460 @@ export class SettingsManager {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Standalone Model Paths section --------------------------------------
|
||||
// The section only exists in standalone mode, where primary folder_paths
|
||||
// are read from settings.json instead of the ComfyUI host. Editors are
|
||||
// rendered from the backend-provided folder_path_schema so new model
|
||||
// categories appear automatically.
|
||||
|
||||
setupModelPathsSection() {
|
||||
if (this.modelPathsSectionInitialized) return;
|
||||
if (!state.global.settings.standalone_mode) return;
|
||||
|
||||
const navGroup = document.querySelector('.settings-nav-list .settings-nav-group');
|
||||
const settingsForm = document.querySelector('.settings-form');
|
||||
if (!navGroup || !settingsForm) return;
|
||||
|
||||
const navButton = document.createElement('button');
|
||||
navButton.type = 'button';
|
||||
navButton.className = 'settings-nav-item';
|
||||
navButton.dataset.section = 'modelPaths';
|
||||
navButton.textContent = translate('settings.nav.modelPaths', {}, 'Model Paths');
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'settings-section';
|
||||
section.id = 'section-modelPaths';
|
||||
section.dataset.section = 'modelPaths';
|
||||
section.innerHTML = `
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection-header">
|
||||
<h4>
|
||||
${translate('settings.modelPaths.title', {}, 'Model Library Paths')}
|
||||
<i class="fas fa-sync-alt restart-required-icon" title="${translate('settings.modelPaths.restartRequired', {}, 'Restart required for changes to take effect')}"></i>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="input-help">
|
||||
${translate('settings.modelPaths.description', {}, 'Root folders LoRA Manager scans for your models. Changes take effect after restarting the server.')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-paths-restart-notice" id="modelPathsRestartNotice">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span>${translate('settings.modelPaths.pendingRestartNotice', {}, 'Path changes saved. Restart LoRA Manager for them to take effect.')}</span>
|
||||
</div>
|
||||
<div class="settings-subsection-header">
|
||||
<h4>${translate('settings.modelPaths.coreTypes', {}, 'Core Model Types')}</h4>
|
||||
</div>
|
||||
<div id="modelPathsCoreTypes"></div>
|
||||
<div class="settings-subsection-header">
|
||||
<h4>${translate('settings.modelPaths.otherTypes', {}, 'Other Model Types')}</h4>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="modelPathsEnableOtherModels">
|
||||
${translate('settings.folderSettings.enableOtherModels', {}, 'Enable Other Models Management')}
|
||||
<i class="fas fa-info-circle info-icon" data-tooltip="${translate('settings.folderSettings.enableOtherModelsHelp', {}, '')}"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="modelPathsEnableOtherModels" onchange="settingsManager.handleModelPathsEnableOtherModels()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item other-subtype-toggles" id="modelPathsSubTypeToggles">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>
|
||||
${translate('settings.folderSettings.otherSubTypes', {}, 'Enabled Model Types')}
|
||||
<i class="fas fa-info-circle info-icon" data-tooltip="${translate('settings.folderSettings.otherSubTypesHelp', {}, '')}"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-control other-subtype-checkboxes">
|
||||
${this._buildModelPathSubTypeCheckboxes()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item" id="modelPathsOtherEmpty">
|
||||
<div class="input-help">
|
||||
${translate('settings.modelPaths.otherTypesDisabledHint', {}, 'No other model types are enabled. Turn on the types you need above to configure their folders.')}
|
||||
</div>
|
||||
</div>
|
||||
<div id="modelPathsOtherTypes"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// The static nav items were bound by initializeNavigation() before the
|
||||
// backend sync completed, so this dynamically added item carries its
|
||||
// own handler with the same show-section behavior.
|
||||
navButton.addEventListener('click', () => {
|
||||
document.querySelectorAll('.settings-section').forEach((s) => s.classList.remove('active'));
|
||||
section.classList.add('active');
|
||||
document.querySelectorAll('.settings-nav-item').forEach((n) => n.classList.remove('active'));
|
||||
navButton.classList.add('active');
|
||||
});
|
||||
|
||||
navGroup.appendChild(navButton);
|
||||
settingsForm.appendChild(section);
|
||||
this.modelPathsSectionInitialized = true;
|
||||
}
|
||||
|
||||
_buildModelPathSubTypeCheckboxes() {
|
||||
const schema = state.global.settings.folder_path_schema || [];
|
||||
const subTypes = [];
|
||||
schema.forEach((entry) => {
|
||||
if (entry.category === 'other' && entry.sub_type && !subTypes.includes(entry.sub_type)) {
|
||||
subTypes.push(entry.sub_type);
|
||||
}
|
||||
});
|
||||
|
||||
return subTypes.map((subType) => {
|
||||
const labelKey = OTHER_SUB_TYPE_LABEL_KEYS[subType];
|
||||
const label = labelKey ? translate(labelKey, {}, subType) : subType;
|
||||
return `
|
||||
<label class="other-subtype-checkbox">
|
||||
<input type="checkbox" value="${subType}"
|
||||
data-model-paths-subtype="${subType}"
|
||||
onchange="settingsManager.handleModelPathsSubTypeToggles()">
|
||||
<span>${label}</span>
|
||||
</label>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Master toggle inside the standalone Model Paths section. Edits the same
|
||||
* enable_other_models key as the Library tab control and keeps both in
|
||||
* sync, then re-renders the other-model path editors in place.
|
||||
*/
|
||||
async handleModelPathsEnableOtherModels() {
|
||||
const toggle = document.getElementById('modelPathsEnableOtherModels');
|
||||
if (!toggle) return;
|
||||
|
||||
const enabled = toggle.checked;
|
||||
const previous = !!state.global.settings.enable_other_models;
|
||||
|
||||
try {
|
||||
await this.saveSetting('enable_other_models', enabled);
|
||||
// Mirror the Library tab flow: refresh its controls and roots,
|
||||
// then re-render this section's editors immediately.
|
||||
this.updateOtherModelsControls();
|
||||
await this.loadOtherRoots();
|
||||
this.updateOtherModelsControls();
|
||||
this.updateOtherModelsNavVisibility(enabled);
|
||||
this.removeOtherModelsAnnouncement(enabled);
|
||||
this.loadModelPaths();
|
||||
showToast('toast.settings.settingsUpdated', { setting: 'enable other models' }, 'success');
|
||||
} catch (error) {
|
||||
toggle.checked = previous;
|
||||
state.global.settings.enable_other_models = previous;
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-type checkboxes inside the standalone Model Paths section. Saves the
|
||||
* same enabled_other_sub_types allow-list as the Library tab checkboxes
|
||||
* (which use data-other-subtype-toggle, so the two never mix) and
|
||||
* re-renders the editors without requiring a modal reopen.
|
||||
*/
|
||||
async handleModelPathsSubTypeToggles() {
|
||||
const values = Array.from(document.querySelectorAll('[data-model-paths-subtype]'))
|
||||
.filter((input) => input.checked)
|
||||
.map((input) => input.value);
|
||||
|
||||
const previous = state.global.settings.enabled_other_sub_types;
|
||||
|
||||
try {
|
||||
await this.saveSetting('enabled_other_sub_types', values);
|
||||
this.updateOtherModelsControls();
|
||||
await this.loadOtherRoots();
|
||||
this.updateOtherModelsControls();
|
||||
this.loadModelPaths();
|
||||
showToast('toast.settings.settingsUpdated', { setting: 'other model types' }, 'success');
|
||||
} catch (error) {
|
||||
state.global.settings.enabled_other_sub_types = previous;
|
||||
this.loadModelPaths();
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadModelPaths() {
|
||||
if (!state.global.settings.standalone_mode) return;
|
||||
|
||||
const coreHost = document.getElementById('modelPathsCoreTypes');
|
||||
const otherHost = document.getElementById('modelPathsOtherTypes');
|
||||
if (!coreHost || !otherHost) return;
|
||||
|
||||
coreHost.innerHTML = '';
|
||||
otherHost.innerHTML = '';
|
||||
|
||||
const schema = state.global.settings.folder_path_schema || [];
|
||||
const otherModelsEnabled = state.global.settings.enable_other_models === true;
|
||||
const enabledSubTypes = new Set(state.global.settings.enabled_other_sub_types || []);
|
||||
|
||||
// Keep the inline enable controls in sync with the current settings.
|
||||
const masterToggle = document.getElementById('modelPathsEnableOtherModels');
|
||||
if (masterToggle) {
|
||||
masterToggle.checked = otherModelsEnabled;
|
||||
}
|
||||
document.querySelectorAll('[data-model-paths-subtype]').forEach((input) => {
|
||||
input.checked = enabledSubTypes.has(input.value);
|
||||
input.disabled = !otherModelsEnabled;
|
||||
});
|
||||
const subTypeToggles = document.getElementById('modelPathsSubTypeToggles');
|
||||
if (subTypeToggles) {
|
||||
subTypeToggles.classList.toggle('is-disabled', !otherModelsEnabled);
|
||||
}
|
||||
|
||||
let otherCount = 0;
|
||||
schema.forEach((entry) => {
|
||||
if (entry.category === 'core') {
|
||||
this._buildModelPathTypeGroup(coreHost, entry);
|
||||
} else if (otherModelsEnabled && entry.sub_type && enabledSubTypes.has(entry.sub_type)) {
|
||||
otherCount++;
|
||||
this._buildModelPathTypeGroup(otherHost, entry);
|
||||
}
|
||||
});
|
||||
|
||||
const emptyHint = document.getElementById('modelPathsOtherEmpty');
|
||||
if (emptyHint) {
|
||||
emptyHint.style.display = otherCount === 0 ? 'block' : 'none';
|
||||
}
|
||||
|
||||
const folderPaths = state.global.settings.folder_paths || {};
|
||||
schema.forEach((entry) => {
|
||||
const container = document.getElementById(`modelFolderPaths-${entry.key}`);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
const paths = folderPaths[entry.key] || [];
|
||||
paths.forEach((path) => {
|
||||
this.addModelFolderPathRow(entry.key, path);
|
||||
});
|
||||
// No trailing empty row on load: an unconfigured type shows just
|
||||
// its Add button, and removing a row never resurrects an empty one.
|
||||
});
|
||||
}
|
||||
|
||||
_buildModelPathTypeGroup(host, entry) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'setting-item';
|
||||
|
||||
const label = translate(`settings.modelPaths.folderKeys.${entry.key}`, {}, entry.key);
|
||||
item.innerHTML = `
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label>${label}</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<button type="button" class="add-mapping-btn" onclick="settingsManager.addModelFolderPathRow('${entry.key}')">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span>${translate('common.actions.add', {}, 'Add')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra-folder-paths-container" id="modelFolderPaths-${entry.key}">
|
||||
</div>
|
||||
`;
|
||||
|
||||
host.appendChild(item);
|
||||
}
|
||||
|
||||
addModelFolderPathRow(key, path = '', shouldFocus = true) {
|
||||
const container = document.getElementById(`modelFolderPaths-${key}`);
|
||||
if (!container) return;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'extra-folder-path-row mapping-row';
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="path-controls">
|
||||
<input type="text" class="extra-folder-path-input"
|
||||
placeholder="${translate('settings.extraFolderPaths.pathPlaceholder', {}, '/path/to/models')}" value="${path}"
|
||||
onblur="settingsManager.updateModelFolderPaths('${key}')"
|
||||
onfocus="settingsManager.clearModelFolderPathError(this)"
|
||||
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
||||
<button type="button" class="remove-path-btn"
|
||||
onclick="settingsManager.removeModelFolderPathRow(this, '${key}')"
|
||||
title="${translate('common.actions.delete', {}, 'Delete')}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="extra-folder-path-error"></div>
|
||||
`;
|
||||
|
||||
container.appendChild(row);
|
||||
|
||||
if (!path && shouldFocus) {
|
||||
const input = row.querySelector('.extra-folder-path-input');
|
||||
if (input) {
|
||||
setTimeout(() => input.focus(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearModelFolderPathError(input) {
|
||||
input.classList.remove('has-error');
|
||||
const row = input.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
const errEl = row.querySelector('.extra-folder-path-error');
|
||||
if (errEl) {
|
||||
errEl.classList.remove('visible');
|
||||
errEl.textContent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllModelFolderPathErrors() {
|
||||
const section = document.getElementById('section-modelPaths');
|
||||
if (!section) return;
|
||||
section.querySelectorAll('.extra-folder-path-input.has-error').forEach((input) => {
|
||||
input.classList.remove('has-error');
|
||||
});
|
||||
section.querySelectorAll('.extra-folder-path-error.visible').forEach((el) => {
|
||||
el.classList.remove('visible');
|
||||
el.textContent = '';
|
||||
});
|
||||
}
|
||||
|
||||
_markModelFolderPathsError(key, overlappingPaths, showMessage = false) {
|
||||
const container = document.getElementById(`modelFolderPaths-${key}`);
|
||||
if (!container) return;
|
||||
|
||||
const inputs = container.querySelectorAll('.extra-folder-path-input');
|
||||
inputs.forEach((input) => {
|
||||
const val = input.value.trim();
|
||||
if (val && overlappingPaths.includes(val)) {
|
||||
input.classList.add('has-error');
|
||||
if (showMessage) {
|
||||
const row = input.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
const errEl = row.querySelector('.extra-folder-path-error');
|
||||
if (errEl) {
|
||||
errEl.textContent = translate('settings.extraFolderPaths.validation.checkpointUnetOverlapInline', {}, 'This path is also used for a different model type. Use separate folders for checkpoints and diffusion models.');
|
||||
errEl.classList.add('visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeModelFolderPathRow(btn, key) {
|
||||
const row = btn.closest('.extra-folder-path-row');
|
||||
if (row) {
|
||||
row.remove();
|
||||
this.updateModelFolderPaths(key, { fromRemoval: true });
|
||||
}
|
||||
}
|
||||
|
||||
async updateModelFolderPaths(changedKey, { fromRemoval = false } = {}) {
|
||||
this._clearAllModelFolderPathErrors();
|
||||
|
||||
const folderPaths = {};
|
||||
|
||||
const section = document.getElementById('section-modelPaths');
|
||||
if (!section) return;
|
||||
|
||||
section.querySelectorAll('.extra-folder-paths-container[id^="modelFolderPaths-"]').forEach((container) => {
|
||||
const key = container.id.slice('modelFolderPaths-'.length);
|
||||
const paths = [];
|
||||
container.querySelectorAll('.extra-folder-path-input').forEach((input) => {
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
paths.push(value);
|
||||
}
|
||||
});
|
||||
folderPaths[key] = paths;
|
||||
});
|
||||
|
||||
// Client-side pre-check: checkpoints and unet must not share the same path.
|
||||
const normalise = (p) => p.replace(/[/\\]+$/, '').toLowerCase();
|
||||
const ckptSet = new Set((folderPaths.checkpoints || []).map(normalise));
|
||||
const unetSet = new Set((folderPaths.unet || []).map(normalise));
|
||||
const ckptOverlap = (folderPaths.checkpoints || []).filter(p => p && unetSet.has(normalise(p)));
|
||||
const unetOverlap = (folderPaths.unet || []).filter(p => p && ckptSet.has(normalise(p)));
|
||||
const hasOverlap = ckptOverlap.length > 0 || unetOverlap.length > 0;
|
||||
|
||||
if (hasOverlap) {
|
||||
if (changedKey === 'checkpoints') {
|
||||
this._markModelFolderPathsError('checkpoints', ckptOverlap, true);
|
||||
this._markModelFolderPathsError('unet', unetOverlap, false);
|
||||
} else if (changedKey === 'unet') {
|
||||
this._markModelFolderPathsError('unet', unetOverlap, true);
|
||||
this._markModelFolderPathsError('checkpoints', ckptOverlap, false);
|
||||
} else {
|
||||
this._markModelFolderPathsError('checkpoints', ckptOverlap, false);
|
||||
this._markModelFolderPathsError('unet', unetOverlap, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPaths = state.global.settings.folder_paths || {};
|
||||
const pathsChanged = JSON.stringify(currentPaths) !== JSON.stringify(folderPaths);
|
||||
|
||||
if (!pathsChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.global.settings.folder_paths = folderPaths;
|
||||
|
||||
try {
|
||||
await this.saveSetting('folder_paths', folderPaths);
|
||||
this._markModelPathsDirty();
|
||||
showToast('settings.modelPaths.saveSuccessRestart', {}, 'success');
|
||||
|
||||
// Keep the continuous-add flow: after the user fills the trailing
|
||||
// empty row, append a fresh one — but never after a removal.
|
||||
const container = document.getElementById(`modelFolderPaths-${changedKey}`);
|
||||
if (container && !fromRemoval) {
|
||||
const inputs = container.querySelectorAll('.extra-folder-path-input');
|
||||
const hasEmptyRow = Array.from(inputs).some((input) => !input.value.trim());
|
||||
|
||||
if (!hasEmptyRow) {
|
||||
this.addModelFolderPathRow(changedKey, '');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save folder paths:', error);
|
||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||
|
||||
state.global.settings.folder_paths = currentPaths;
|
||||
this.loadModelPaths();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface a persistent "restart required" cue after folder_paths changes:
|
||||
* a dot on the Model Paths nav item, an inline notice in the section, and
|
||||
* a global banner. The banner id is unique per change because dismissed
|
||||
* banner ids persist across restarts — reusing one would mute future
|
||||
* reminders. The whole state clears on the next page load (i.e. after the
|
||||
* restart the user was asked to do).
|
||||
*/
|
||||
_markModelPathsDirty() {
|
||||
if (this.modelPathsDirty) return;
|
||||
this.modelPathsDirty = true;
|
||||
|
||||
document.querySelector('.settings-nav-item[data-section="modelPaths"]')
|
||||
?.classList.add('has-pending-restart');
|
||||
|
||||
document.getElementById('modelPathsRestartNotice')?.classList.add('visible');
|
||||
|
||||
const bannerId = `model-paths-restart-${Date.now()}`;
|
||||
bannerService.registerBanner(bannerId, {
|
||||
id: bannerId,
|
||||
title: translate('settings.modelPaths.pendingRestartBannerTitle', {}, 'Restart required to apply path changes'),
|
||||
content: translate('settings.modelPaths.pendingRestartBannerMessage', {}, 'Model library paths were updated. Restart the LoRA Manager server to scan the new folders.'),
|
||||
dismissible: true,
|
||||
priority: 60,
|
||||
});
|
||||
}
|
||||
|
||||
loadBaseModelMappings() {
|
||||
const mappingsContainer = document.getElementById('baseModelMappingsContainer');
|
||||
if (!mappingsContainer) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { appCore } from './core.js';
|
||||
import { showToast } from './utils/uiHelpers.js';
|
||||
import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.js';
|
||||
import { enableOtherModels, openOtherModelsSettings, openModelPathsSettings } from './utils/otherModels.js';
|
||||
|
||||
/**
|
||||
* Other Models is an opt-in feature. While it is disabled this page renders an
|
||||
@@ -9,8 +9,8 @@ import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.
|
||||
*
|
||||
* The same module backs the "enabled but no folders found" state: ComfyUI
|
||||
* mode points to the Settings page's Library section, while standalone mode
|
||||
* (where the settings UI cannot edit primary folder paths) reveals the
|
||||
* settings.json file the user must edit instead.
|
||||
* points to the standalone-only Model Paths section (which edits the primary
|
||||
* folder_paths) and still offers the settings.json location as a fallback.
|
||||
*/
|
||||
async function handleEnableClick() {
|
||||
const button = document.getElementById('enableOtherModelsBtn');
|
||||
@@ -35,9 +35,17 @@ function handleOpenSettingsClick(event) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings.json location from the standalone no-folders state.
|
||||
* The settings UI cannot edit primary folder_paths, so the only useful
|
||||
* action is revealing the file itself (or copying its path in Docker).
|
||||
* Open Settings on the Model Paths section for the standalone "no folders
|
||||
* found" state, so the missing folders can be added directly.
|
||||
*/
|
||||
function handleOpenModelPathsSettingsClick(event) {
|
||||
event.preventDefault();
|
||||
openModelPathsSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings.json location from the standalone no-folders state,
|
||||
* offered as a fallback next to the Model Paths settings button.
|
||||
*/
|
||||
async function handleOpenSettingsFolderClick() {
|
||||
const button = document.getElementById('openSettingsFolderBtn');
|
||||
@@ -84,6 +92,11 @@ async function initializeOtherDisabledPage() {
|
||||
settingsButton.addEventListener('click', handleOpenSettingsClick);
|
||||
}
|
||||
|
||||
const modelPathsButton = document.getElementById('openModelPathsSettingsBtn');
|
||||
if (modelPathsButton) {
|
||||
modelPathsButton.addEventListener('click', handleOpenModelPathsSettingsClick);
|
||||
}
|
||||
|
||||
const settingsFolderButton = document.getElementById('openSettingsFolderBtn');
|
||||
if (settingsFolderButton) {
|
||||
settingsFolderButton.addEventListener('click', handleOpenSettingsFolderClick);
|
||||
|
||||
@@ -77,6 +77,12 @@ export function createDefaultSettings() {
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
default_other_roots: {},
|
||||
enabled_other_sub_types: ['vae', 'upscaler', 'text_encoder'],
|
||||
// Standalone-only fields populated by GET /api/lm/settings; in plugin
|
||||
// mode the backend omits folder_paths/folder_path_schema and these
|
||||
// defaults apply.
|
||||
standalone_mode: false,
|
||||
folder_paths: {},
|
||||
folder_path_schema: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,3 +50,19 @@ export function openOtherModelsSettings() {
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings modal on the standalone-only Model Paths section, where
|
||||
* primary folder_paths are edited. The section only exists in standalone mode,
|
||||
* so the nav item lookup simply no-ops elsewhere.
|
||||
*/
|
||||
export function openModelPathsSettings() {
|
||||
const modalManager = window.modalManager;
|
||||
if (modalManager && typeof modalManager.showModal === 'function') {
|
||||
modalManager.showModal('settingsModal');
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
document.querySelector('.settings-nav-item[data-section="modelPaths"]')?.click();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
+4
-20
@@ -51,18 +51,6 @@
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.other-no-paths-config {
|
||||
margin: 4px 0 0;
|
||||
padding: 12px 16px;
|
||||
max-width: 520px;
|
||||
overflow-x: auto;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 6px;
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||
}
|
||||
.other-settings-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -140,17 +128,13 @@
|
||||
<h2>{{ t('other.noPaths.title') }}</h2>
|
||||
{% if standalone_mode %}
|
||||
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
|
||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
||||
<button id="openModelPathsSettingsBtn" type="button">
|
||||
<i class="fas fa-cog"></i> {{ t('other.noPaths.openModelPaths') }}
|
||||
</button>
|
||||
{% if settings_file %}
|
||||
<p class="other-settings-file"><i class="fas fa-file-alt"></i> <code>{{ settings_file }}</code></p>
|
||||
{% endif %}
|
||||
<pre class="other-no-paths-config"><code>"folder_paths": {
|
||||
"vae": ["/path/to/vae"],
|
||||
"upscale_models": ["/path/to/upscale_models"],
|
||||
"text_encoders": ["/path/to/text_encoders"],
|
||||
"clip_vision": ["/path/to/clip_vision"],
|
||||
"controlnet": ["/path/to/controlnet"]
|
||||
}</code></pre>
|
||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
||||
<button id="openSettingsFolderBtn" type="button">
|
||||
<i class="fas fa-folder-open"></i> {{ t('other.noPaths.openSettingsFolder') }}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../static/js/managers/ModalManager.js', () => ({
|
||||
modalManager: {
|
||||
closeModal: vi.fn(),
|
||||
showModal: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/state/index.js', () => {
|
||||
return {
|
||||
state: {
|
||||
global: {
|
||||
settings: {},
|
||||
},
|
||||
},
|
||||
createDefaultSettings: () => ({
|
||||
language: 'en',
|
||||
standalone_mode: false,
|
||||
folder_paths: {},
|
||||
folder_path_schema: [],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
||||
resetAndReload: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/constants.js', () => ({
|
||||
DOWNLOAD_PATH_TEMPLATES: {},
|
||||
DEFAULT_PATH_TEMPLATES: {},
|
||||
MAPPABLE_BASE_MODELS: [],
|
||||
PATH_TEMPLATE_PLACEHOLDERS: {},
|
||||
DEFAULT_PRIORITY_TAG_CONFIG: {},
|
||||
getMappableBaseModelsDynamic: () => [],
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({
|
||||
translate: (_key, _params, fallback) => fallback ?? '',
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/i18n/index.js', () => ({
|
||||
i18n: {
|
||||
getCurrentLocale: () => 'en',
|
||||
setLanguage: vi.fn().mockResolvedValue(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/components/shared/ModelCard.js', () => ({
|
||||
configureModelCardVideo: vi.fn(),
|
||||
}));
|
||||
|
||||
import { SettingsManager } from '../../../static/js/managers/SettingsManager.js';
|
||||
import { bannerService } from '../../../static/js/managers/BannerService.js';
|
||||
import { state } from '../../../static/js/state/index.js';
|
||||
|
||||
const CORE_SCHEMA = [
|
||||
{ key: 'loras', category: 'core', sub_type: null },
|
||||
{ key: 'checkpoints', category: 'core', sub_type: null },
|
||||
{ key: 'unet', category: 'core', sub_type: null },
|
||||
{ key: 'embeddings', category: 'core', sub_type: null },
|
||||
];
|
||||
|
||||
const OTHER_SCHEMA = [
|
||||
{ key: 'vae', category: 'other', sub_type: 'vae' },
|
||||
{ key: 'controlnet', category: 'other', sub_type: 'controlnet' },
|
||||
];
|
||||
|
||||
const createManager = () => {
|
||||
const initSettingsSpy = vi
|
||||
.spyOn(SettingsManager.prototype, 'initializeSettings')
|
||||
.mockResolvedValue();
|
||||
const initializeSpy = vi
|
||||
.spyOn(SettingsManager.prototype, 'initialize')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
const manager = new SettingsManager();
|
||||
|
||||
initSettingsSpy.mockRestore();
|
||||
initializeSpy.mockRestore();
|
||||
|
||||
return manager;
|
||||
};
|
||||
|
||||
const buildModalDom = () => {
|
||||
document.body.innerHTML = `
|
||||
<nav class="settings-nav">
|
||||
<ul class="settings-nav-list">
|
||||
<li class="settings-nav-group">
|
||||
<button type="button" class="settings-nav-item active" data-section="general">General</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="settings-form">
|
||||
<div class="settings-section active" id="section-general" data-section="general"></div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const setStandaloneSettings = (overrides = {}) => {
|
||||
state.global.settings = {
|
||||
standalone_mode: true,
|
||||
folder_paths: {},
|
||||
folder_path_schema: [...CORE_SCHEMA, ...OTHER_SCHEMA],
|
||||
enable_other_models: false,
|
||||
enabled_other_sub_types: [],
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
vi.clearAllMocks();
|
||||
bannerService.banners.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete global.fetch;
|
||||
});
|
||||
|
||||
describe('SettingsManager Model Paths section', () => {
|
||||
it('does not create the section in plugin mode', () => {
|
||||
buildModalDom();
|
||||
state.global.settings = { standalone_mode: false };
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
|
||||
expect(document.querySelector('.settings-nav-item[data-section="modelPaths"]')).toBeNull();
|
||||
expect(document.getElementById('section-modelPaths')).toBeNull();
|
||||
});
|
||||
|
||||
it('creates nav item and section in standalone mode', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
|
||||
expect(document.querySelector('.settings-nav-item[data-section="modelPaths"]')).not.toBeNull();
|
||||
expect(document.getElementById('section-modelPaths')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('switches sections when the nav item is clicked', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
|
||||
document.querySelector('.settings-nav-item[data-section="modelPaths"]').click();
|
||||
|
||||
expect(document.getElementById('section-modelPaths').classList.contains('active')).toBe(true);
|
||||
expect(document.getElementById('section-general').classList.contains('active')).toBe(false);
|
||||
});
|
||||
|
||||
it('static nav clicks clear the Model Paths active state (regression)', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
// Static nav items were bound before the Model Paths button existed;
|
||||
// their handler must still clear its active state.
|
||||
manager.initializeNavigation();
|
||||
|
||||
const modelPathsNav = document.querySelector('.settings-nav-item[data-section="modelPaths"]');
|
||||
modelPathsNav.click();
|
||||
expect(modelPathsNav.classList.contains('active')).toBe(true);
|
||||
|
||||
document.querySelector('.settings-nav-item[data-section="general"]').click();
|
||||
|
||||
expect(modelPathsNav.classList.contains('active')).toBe(false);
|
||||
expect(document.getElementById('section-modelPaths').classList.contains('active')).toBe(false);
|
||||
expect(document.getElementById('section-general').classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders core editors and only enabled other-model editors', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({
|
||||
enable_other_models: true,
|
||||
enabled_other_sub_types: ['vae'],
|
||||
folder_paths: { loras: ['/models/loras'] },
|
||||
});
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
// Core editors always rendered
|
||||
CORE_SCHEMA.forEach(({ key }) => {
|
||||
expect(document.getElementById(`modelFolderPaths-${key}`)).not.toBeNull();
|
||||
});
|
||||
|
||||
// Only the enabled other-model sub-type is rendered
|
||||
expect(document.getElementById('modelFolderPaths-vae')).not.toBeNull();
|
||||
expect(document.getElementById('modelFolderPaths-controlnet')).toBeNull();
|
||||
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('none');
|
||||
|
||||
// Existing values populate rows
|
||||
const loraInput = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
|
||||
expect(loraInput.value).toBe('/models/loras');
|
||||
});
|
||||
|
||||
it('shows the empty hint when no other-model types are enabled', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('block');
|
||||
expect(document.getElementById('modelFolderPaths-vae')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders inline enable controls synced with current settings', () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({
|
||||
enable_other_models: true,
|
||||
enabled_other_sub_types: ['vae'],
|
||||
});
|
||||
|
||||
const manager = createManager();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
const master = document.getElementById('modelPathsEnableOtherModels');
|
||||
expect(master).not.toBeNull();
|
||||
expect(master.checked).toBe(true);
|
||||
|
||||
const vaeBox = document.querySelector('[data-model-paths-subtype="vae"]');
|
||||
const controlnetBox = document.querySelector('[data-model-paths-subtype="controlnet"]');
|
||||
expect(vaeBox.checked).toBe(true);
|
||||
expect(vaeBox.disabled).toBe(false);
|
||||
expect(controlnetBox.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('inline master toggle saves the setting and re-renders editors', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({
|
||||
enable_other_models: true,
|
||||
enabled_other_sub_types: ['vae'],
|
||||
});
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
|
||||
state.global.settings[key] = value;
|
||||
});
|
||||
manager.loadOtherRoots = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
expect(document.getElementById('modelFolderPaths-vae')).not.toBeNull();
|
||||
|
||||
const master = document.getElementById('modelPathsEnableOtherModels');
|
||||
master.checked = false;
|
||||
await manager.handleModelPathsEnableOtherModels();
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalledWith('enable_other_models', false);
|
||||
expect(state.global.settings.enable_other_models).toBe(false);
|
||||
// Editors removed in place, empty hint back
|
||||
expect(document.getElementById('modelFolderPaths-vae')).toBeNull();
|
||||
expect(document.getElementById('modelPathsOtherEmpty').style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('inline sub-type checkboxes save the allow-list and re-render editors', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({
|
||||
enable_other_models: true,
|
||||
enabled_other_sub_types: ['vae'],
|
||||
});
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockImplementation(async (key, value) => {
|
||||
state.global.settings[key] = value;
|
||||
});
|
||||
manager.loadOtherRoots = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
document.querySelector('[data-model-paths-subtype="controlnet"]').checked = true;
|
||||
await manager.handleModelPathsSubTypeToggles();
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalledWith('enabled_other_sub_types', ['vae', 'controlnet']);
|
||||
expect(document.getElementById('modelFolderPaths-controlnet')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('saves collected folder paths via saveSetting', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
// No rows exist until the user clicks Add
|
||||
expect(document.querySelector('#modelFolderPaths-loras .extra-folder-path-input')).toBeNull();
|
||||
|
||||
manager.addModelFolderPathRow('loras');
|
||||
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
|
||||
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
expect(manager.saveSetting).toHaveBeenCalledWith('folder_paths', {
|
||||
loras: ['/data/loras'],
|
||||
checkpoints: [],
|
||||
unet: [],
|
||||
embeddings: [],
|
||||
});
|
||||
expect(state.global.settings.folder_paths.loras).toEqual(['/data/loras']);
|
||||
});
|
||||
|
||||
it('blocks saving when checkpoints and unet share a path', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
manager.addModelFolderPathRow('checkpoints');
|
||||
manager.addModelFolderPathRow('unet');
|
||||
document.querySelector('#modelFolderPaths-checkpoints .extra-folder-path-input').value = '/same/dir';
|
||||
document.querySelector('#modelFolderPaths-unet .extra-folder-path-input').value = '/same/dir';
|
||||
|
||||
await manager.updateModelFolderPaths('checkpoints');
|
||||
|
||||
expect(manager.saveSetting).not.toHaveBeenCalled();
|
||||
const ckptInput = document.querySelector('#modelFolderPaths-checkpoints .extra-folder-path-input');
|
||||
expect(ckptInput.classList.contains('has-error')).toBe(true);
|
||||
});
|
||||
|
||||
it('appends a trailing empty row after filling one, but not after a removal', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({ folder_paths: { loras: ['/data/a'] } });
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
// Fill the trailing empty row -> save appends a fresh empty row
|
||||
manager.addModelFolderPathRow('loras');
|
||||
const rows = () => document.querySelectorAll('#modelFolderPaths-loras .extra-folder-path-row');
|
||||
expect(rows()).toHaveLength(2);
|
||||
rows()[1].querySelector('.extra-folder-path-input').value = '/data/b';
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
expect(rows()).toHaveLength(3);
|
||||
|
||||
// Removing a row never resurrects an empty row
|
||||
const removeBtn = rows()[0].querySelector('.remove-path-btn');
|
||||
manager.removeModelFolderPathRow(removeBtn, 'loras');
|
||||
await vi.waitFor(() => {
|
||||
expect(manager.saveSetting).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
// Two rows left: the saved '/data/b' plus the pre-existing trailing
|
||||
// empty row — removal must not append yet another empty row.
|
||||
expect(rows()).toHaveLength(2);
|
||||
const emptyRows = Array.from(rows()).filter(
|
||||
(row) => row.querySelector('.extra-folder-path-input').value === '',
|
||||
);
|
||||
expect(emptyRows).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('restores previous state when saving fails', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings({ folder_paths: { loras: ['/original'] } });
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockRejectedValue(new Error('nope'));
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
const input = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
|
||||
input.value = '/changed';
|
||||
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
expect(state.global.settings.folder_paths).toEqual({ loras: ['/original'] });
|
||||
// Rows reloaded from restored state
|
||||
const reloaded = document.querySelector('#modelFolderPaths-loras .extra-folder-path-input');
|
||||
expect(reloaded.value).toBe('/original');
|
||||
});
|
||||
|
||||
it('marks pending-restart cues after a successful save', async () => {
|
||||
buildModalDom();
|
||||
setStandaloneSettings();
|
||||
|
||||
const manager = createManager();
|
||||
manager.saveSetting = vi.fn().mockResolvedValue();
|
||||
manager.setupModelPathsSection();
|
||||
manager.loadModelPaths();
|
||||
|
||||
const navItem = document.querySelector('.settings-nav-item[data-section="modelPaths"]');
|
||||
expect(navItem.classList.contains('has-pending-restart')).toBe(false);
|
||||
|
||||
manager.addModelFolderPathRow('loras');
|
||||
document.querySelector('#modelFolderPaths-loras .extra-folder-path-input').value = '/data/loras';
|
||||
await manager.updateModelFolderPaths('loras');
|
||||
|
||||
expect(navItem.classList.contains('has-pending-restart')).toBe(true);
|
||||
expect(document.getElementById('modelPathsRestartNotice').classList.contains('visible')).toBe(true);
|
||||
|
||||
// A unique-per-change banner id: dismissing it once must not mute
|
||||
// future reminders (dismissed ids persist across restarts).
|
||||
const restartBanners = Array.from(bannerService.banners.keys())
|
||||
.filter((id) => id.startsWith('model-paths-restart-'));
|
||||
expect(restartBanners).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,7 @@ describe('Other Models disabled page', () => {
|
||||
document.body.innerHTML = [
|
||||
'<button id="enableOtherModelsBtn"></button>',
|
||||
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||
'<button id="openModelPathsSettingsBtn"></button>',
|
||||
'<button id="openSettingsFolderBtn"></button>',
|
||||
].join('');
|
||||
|
||||
@@ -65,6 +66,27 @@ describe('Other Models disabled page', () => {
|
||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||
});
|
||||
|
||||
it('opens the Model Paths settings from the standalone no-folders state', async () => {
|
||||
const showModal = vi.fn();
|
||||
window.modalManager = { showModal };
|
||||
|
||||
const navItem = document.createElement('button');
|
||||
navItem.className = 'settings-nav-item';
|
||||
navItem.dataset.section = 'modelPaths';
|
||||
const navClick = vi.fn();
|
||||
navItem.addEventListener('click', navClick);
|
||||
document.body.appendChild(navItem);
|
||||
|
||||
document.getElementById('openModelPathsSettingsBtn').dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true }),
|
||||
);
|
||||
|
||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
expect(navClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('reveals the settings.json location from the standalone no-folders state', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
'language': 'en',
|
||||
'llm_api_key_set': False,
|
||||
'other_models_paths_available': False,
|
||||
'standalone_mode': False,
|
||||
'theme': 'dark',
|
||||
}),
|
||||
'success': True,
|
||||
|
||||
@@ -160,3 +160,134 @@ async def test_activate_library_unexpected_error_returns_500(monkeypatch):
|
||||
assert response.status == 500
|
||||
assert payload["success"] is False
|
||||
assert payload["error"] == "bad things"
|
||||
|
||||
|
||||
class DummySettingsForGet:
|
||||
def __init__(self, values=None):
|
||||
self._values = dict(values or {})
|
||||
self.settings_file = "/tmp/settings.json"
|
||||
self.set_calls = []
|
||||
|
||||
def keys(self):
|
||||
return self._values.keys()
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._values.get(key, default)
|
||||
|
||||
def set(self, key, value):
|
||||
self.set_calls.append((key, value))
|
||||
self._values[key] = value
|
||||
|
||||
def get_startup_messages(self):
|
||||
return []
|
||||
|
||||
|
||||
def make_get_handler(values=None) -> SettingsHandler:
|
||||
return SettingsHandler(
|
||||
settings_service=DummySettingsForGet(values),
|
||||
metadata_provider_updater=noop_async,
|
||||
downloader_factory=dummy_downloader_factory,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_other_models_availability(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
config,
|
||||
"get_other_models_availability",
|
||||
lambda: {"available": False},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_settings_plugin_mode_hides_folder_paths(
|
||||
monkeypatch, patch_other_models_availability
|
||||
):
|
||||
monkeypatch.delenv("LORA_MANAGER_STANDALONE", raising=False)
|
||||
handler = make_get_handler(
|
||||
{
|
||||
"language": "en",
|
||||
"folder_paths": {"loras": ["/models/loras"]},
|
||||
}
|
||||
)
|
||||
|
||||
response = await handler.get_settings(FakeRequest())
|
||||
payload = json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
settings = payload["settings"]
|
||||
assert settings["standalone_mode"] is False
|
||||
assert "folder_paths" not in settings
|
||||
assert "folder_path_schema" not in settings
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_settings_standalone_exposes_folder_paths_and_schema(
|
||||
monkeypatch, patch_other_models_availability
|
||||
):
|
||||
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
||||
folder_paths = {"loras": ["/models/loras"], "vae": ["/models/vae"]}
|
||||
handler = make_get_handler({"language": "en", "folder_paths": folder_paths})
|
||||
|
||||
response = await handler.get_settings(FakeRequest())
|
||||
payload = json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
settings = payload["settings"]
|
||||
assert settings["standalone_mode"] is True
|
||||
assert settings["folder_paths"] == folder_paths
|
||||
|
||||
schema = settings["folder_path_schema"]
|
||||
core_keys = [entry["key"] for entry in schema if entry["category"] == "core"]
|
||||
assert core_keys == ["loras", "checkpoints", "unet", "embeddings"]
|
||||
other_entries = {entry["key"]: entry for entry in schema if entry["category"] == "other"}
|
||||
assert other_entries["vae"]["sub_type"] == "vae"
|
||||
assert other_entries["text_encoders"]["sub_type"] == "text_encoder"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_settings_passes_folder_paths_through(
|
||||
monkeypatch, patch_other_models_availability
|
||||
):
|
||||
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
||||
handler = make_get_handler({"folder_paths": {}})
|
||||
new_paths = {"loras": ["/models/loras"]}
|
||||
|
||||
response = await handler.update_settings(
|
||||
FakeRequest(json_data={"folder_paths": new_paths})
|
||||
)
|
||||
payload = json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["success"] is True
|
||||
assert handler._settings.set_calls == [("folder_paths", new_paths)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_settings_standalone_filters_template_placeholders(
|
||||
monkeypatch, patch_other_models_availability
|
||||
):
|
||||
"""Fresh installs are seeded from settings.json.example; its placeholder
|
||||
paths must not show up as real values in the Model Paths UI."""
|
||||
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
||||
handler = make_get_handler(
|
||||
{
|
||||
"folder_paths": {
|
||||
"loras": ["C:/path/to/your/loras_folder", "/real/loras"],
|
||||
"vae": ["C:/path/to/another/vae_folder"],
|
||||
}
|
||||
}
|
||||
)
|
||||
handler._settings.get_template_folder_path_placeholders = lambda: {
|
||||
"C:/path/to/your/loras_folder",
|
||||
"C:/path/to/another/vae_folder",
|
||||
}
|
||||
|
||||
response = await handler.get_settings(FakeRequest())
|
||||
payload = json_payload(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["settings"]["folder_paths"] == {
|
||||
"loras": ["/real/loras"],
|
||||
"vae": [],
|
||||
}
|
||||
|
||||
@@ -52,12 +52,18 @@ def test_missing_settings_creates_defaults_and_emits_warnings(tmp_path):
|
||||
|
||||
actions = warning.get("actions") or []
|
||||
assert actions == [
|
||||
{
|
||||
"action": "open-model-paths-settings",
|
||||
"label": "Configure model folders",
|
||||
"type": "primary",
|
||||
"icon": "fas fa-cog",
|
||||
},
|
||||
{
|
||||
"action": "open-settings-location",
|
||||
"label": "Open settings folder",
|
||||
"type": "primary",
|
||||
"icon": "fas fa-folder-open",
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -155,3 +161,13 @@ def test_apply_settings_dir_from_argv():
|
||||
os.environ.pop("LORA_MANAGER_SETTINGS_DIR", None)
|
||||
else:
|
||||
os.environ["LORA_MANAGER_SETTINGS_DIR"] = previous
|
||||
|
||||
|
||||
def test_template_folder_path_placeholders_are_exposed():
|
||||
manager = get_settings_manager()
|
||||
|
||||
placeholders = manager.get_template_folder_path_placeholders()
|
||||
|
||||
assert "C:/path/to/your/loras_folder" in placeholders
|
||||
assert "C:/path/to/another/embeddings_folder" in placeholders
|
||||
assert len(placeholders) == 8
|
||||
|
||||
@@ -42,4 +42,36 @@ class TestIsEmptyPlaceholderHash:
|
||||
|
||||
def test_rejects_non_strings(self):
|
||||
assert not is_empty_placeholder_hash(None)
|
||||
assert not is_empty_placeholder_hash(123)
|
||||
assert not is_empty_placeholder_hash(123)
|
||||
|
||||
class TestFolderPathSchema:
|
||||
def test_core_keys_first_in_canonical_order(self):
|
||||
from py.utils.constants import CORE_FOLDER_PATH_KEYS, folder_path_schema
|
||||
|
||||
schema = folder_path_schema()
|
||||
core = [entry for entry in schema if entry["category"] == "core"]
|
||||
|
||||
assert [entry["key"] for entry in core] == CORE_FOLDER_PATH_KEYS
|
||||
assert all(entry["sub_type"] is None for entry in core)
|
||||
assert schema[: len(core)] == core
|
||||
|
||||
def test_other_entries_derive_from_subtypes_table(self):
|
||||
from py.utils.constants import OTHER_MODEL_FOLDER_SUBTYPES, folder_path_schema
|
||||
|
||||
schema = folder_path_schema()
|
||||
other = {entry["key"]: entry for entry in schema if entry["category"] == "other"}
|
||||
|
||||
assert set(other) == set(OTHER_MODEL_FOLDER_SUBTYPES)
|
||||
for folder_key, sub_type in OTHER_MODEL_FOLDER_SUBTYPES.items():
|
||||
assert other[folder_key]["sub_type"] == sub_type
|
||||
|
||||
def test_text_encoder_exposes_both_folder_keys(self):
|
||||
from py.utils.constants import folder_path_schema
|
||||
|
||||
text_encoder_keys = [
|
||||
entry["key"]
|
||||
for entry in folder_path_schema()
|
||||
if entry["sub_type"] == "text_encoder"
|
||||
]
|
||||
|
||||
assert text_encoder_keys == ["text_encoders", "clip"]
|
||||
|
||||
Reference in New Issue
Block a user