mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
feat(frontend): enable downloads on Other page and default_other_roots settings UI
This commit is contained in:
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "Legen Sie den Standard-Diffusion-Modell-(UNET)-Stammordner für Downloads, Importe und Verschiebungen fest",
|
"defaultUnetRootHelp": "Legen Sie den Standard-Diffusion-Modell-(UNET)-Stammordner für Downloads, Importe und Verschiebungen fest",
|
||||||
"defaultEmbeddingRoot": "Embedding-Stammordner",
|
"defaultEmbeddingRoot": "Embedding-Stammordner",
|
||||||
"defaultEmbeddingRootHelp": "Legen Sie den Standard-Embedding-Stammordner für Downloads, Importe und Verschiebungen fest",
|
"defaultEmbeddingRootHelp": "Legen Sie den Standard-Embedding-Stammordner für Downloads, Importe und Verschiebungen fest",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "Rezepte-Speicherpfad",
|
"recipesPath": "Rezepte-Speicherpfad",
|
||||||
"recipesPathHelp": "Optionales benutzerdefiniertes Verzeichnis für gespeicherte Rezepte. Leer lassen, um den recipes-Ordner im ersten LoRA-Stammverzeichnis zu verwenden.",
|
"recipesPathHelp": "Optionales benutzerdefiniertes Verzeichnis für gespeicherte Rezepte. Leer lassen, um den recipes-Ordner im ersten LoRA-Stammverzeichnis zu verwenden.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Fehler beim Laden der Checkpoint-Stammverzeichnisse: {message}",
|
"checkpointRootsFailed": "Fehler beim Laden der Checkpoint-Stammverzeichnisse: {message}",
|
||||||
"unetRootsFailed": "Fehler beim Laden der Diffusion-Modell-Stammverzeichnisse: {message}",
|
"unetRootsFailed": "Fehler beim Laden der Diffusion-Modell-Stammverzeichnisse: {message}",
|
||||||
"embeddingRootsFailed": "Fehler beim Laden der Embedding-Stammverzeichnisse: {message}",
|
"embeddingRootsFailed": "Fehler beim Laden der Embedding-Stammverzeichnisse: {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "Basismodell-Pfad-Zuordnungen aktualisiert ({count})",
|
"mappingsUpdated": "Basismodell-Pfad-Zuordnungen aktualisiert ({count})",
|
||||||
"mappingsCleared": "Basismodell-Pfad-Zuordnungen gelöscht",
|
"mappingsCleared": "Basismodell-Pfad-Zuordnungen gelöscht",
|
||||||
"mappingSaveFailed": "Fehler beim Speichern der Basismodell-Zuordnungen: {message}",
|
"mappingSaveFailed": "Fehler beim Speichern der Basismodell-Zuordnungen: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "Set default diffusion model (UNET) root directory for downloads, imports and moves",
|
"defaultUnetRootHelp": "Set default diffusion model (UNET) root directory for downloads, imports and moves",
|
||||||
"defaultEmbeddingRoot": "Embedding Root",
|
"defaultEmbeddingRoot": "Embedding Root",
|
||||||
"defaultEmbeddingRootHelp": "Set default embedding root directory for downloads, imports and moves",
|
"defaultEmbeddingRootHelp": "Set default embedding root directory for downloads, imports and moves",
|
||||||
|
"defaultVaeRoot": "VAE Root",
|
||||||
|
"defaultVaeRootHelp": "Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "Recipes Storage Path",
|
"recipesPath": "Recipes Storage Path",
|
||||||
"recipesPathHelp": "Optional custom directory for stored recipes. Leave empty to use the first LoRA root's recipes folder.",
|
"recipesPathHelp": "Optional custom directory for stored recipes. Leave empty to use the first LoRA root's recipes folder.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Failed to load checkpoint roots: {message}",
|
"checkpointRootsFailed": "Failed to load checkpoint roots: {message}",
|
||||||
"unetRootsFailed": "Failed to load diffusion model roots: {message}",
|
"unetRootsFailed": "Failed to load diffusion model roots: {message}",
|
||||||
"embeddingRootsFailed": "Failed to load embedding roots: {message}",
|
"embeddingRootsFailed": "Failed to load embedding roots: {message}",
|
||||||
|
"otherRootsFailed": "Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "Base model path mappings updated ({count} mapping{plural})",
|
"mappingsUpdated": "Base model path mappings updated ({count} mapping{plural})",
|
||||||
"mappingsCleared": "Base model path mappings cleared",
|
"mappingsCleared": "Base model path mappings cleared",
|
||||||
"mappingSaveFailed": "Failed to save base model mappings: {message}",
|
"mappingSaveFailed": "Failed to save base model mappings: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "Establecer el directorio raíz predeterminado de Diffusion Model (UNET) para descargas, importaciones y movimientos",
|
"defaultUnetRootHelp": "Establecer el directorio raíz predeterminado de Diffusion Model (UNET) para descargas, importaciones y movimientos",
|
||||||
"defaultEmbeddingRoot": "Raíz de embedding",
|
"defaultEmbeddingRoot": "Raíz de embedding",
|
||||||
"defaultEmbeddingRootHelp": "Establecer el directorio raíz predeterminado de embedding para descargas, importaciones y movimientos",
|
"defaultEmbeddingRootHelp": "Establecer el directorio raíz predeterminado de embedding para descargas, importaciones y movimientos",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "Ruta de almacenamiento de recetas",
|
"recipesPath": "Ruta de almacenamiento de recetas",
|
||||||
"recipesPathHelp": "Directorio personalizado opcional para las recetas guardadas. Déjalo vacío para usar la carpeta recipes del primer directorio raíz de LoRA.",
|
"recipesPathHelp": "Directorio personalizado opcional para las recetas guardadas. Déjalo vacío para usar la carpeta recipes del primer directorio raíz de LoRA.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Error al cargar raíces de checkpoint: {message}",
|
"checkpointRootsFailed": "Error al cargar raíces de checkpoint: {message}",
|
||||||
"unetRootsFailed": "Error al cargar raíces de Diffusion Model: {message}",
|
"unetRootsFailed": "Error al cargar raíces de Diffusion Model: {message}",
|
||||||
"embeddingRootsFailed": "Error al cargar raíces de embedding: {message}",
|
"embeddingRootsFailed": "Error al cargar raíces de embedding: {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "Mapeos de rutas de modelo base actualizados ({count} mapeo{plural})",
|
"mappingsUpdated": "Mapeos de rutas de modelo base actualizados ({count} mapeo{plural})",
|
||||||
"mappingsCleared": "Mapeos de rutas de modelo base limpiados",
|
"mappingsCleared": "Mapeos de rutas de modelo base limpiados",
|
||||||
"mappingSaveFailed": "Error al guardar mapeos de modelo base: {message}",
|
"mappingSaveFailed": "Error al guardar mapeos de modelo base: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "Définir le répertoire racine Diffusion Model (UNET) par défaut pour les téléchargements, imports et déplacements",
|
"defaultUnetRootHelp": "Définir le répertoire racine Diffusion Model (UNET) par défaut pour les téléchargements, imports et déplacements",
|
||||||
"defaultEmbeddingRoot": "Racine Embedding",
|
"defaultEmbeddingRoot": "Racine Embedding",
|
||||||
"defaultEmbeddingRootHelp": "Définir le répertoire racine embedding par défaut pour les téléchargements, imports et déplacements",
|
"defaultEmbeddingRootHelp": "Définir le répertoire racine embedding par défaut pour les téléchargements, imports et déplacements",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "Chemin de stockage des Recipes",
|
"recipesPath": "Chemin de stockage des Recipes",
|
||||||
"recipesPathHelp": "Dossier personnalisé facultatif pour les Recipes enregistrées. Laissez vide pour utiliser le dossier recipes de la première racine LoRA.",
|
"recipesPathHelp": "Dossier personnalisé facultatif pour les Recipes enregistrées. Laissez vide pour utiliser le dossier recipes de la première racine LoRA.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Échec du chargement des racines checkpoint : {message}",
|
"checkpointRootsFailed": "Échec du chargement des racines checkpoint : {message}",
|
||||||
"unetRootsFailed": "Échec du chargement des racines Diffusion Model : {message}",
|
"unetRootsFailed": "Échec du chargement des racines Diffusion Model : {message}",
|
||||||
"embeddingRootsFailed": "Échec du chargement des racines embedding : {message}",
|
"embeddingRootsFailed": "Échec du chargement des racines embedding : {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "Mappages de chemin de modèle de base mis à jour ({count} mappage{plural})",
|
"mappingsUpdated": "Mappages de chemin de modèle de base mis à jour ({count} mappage{plural})",
|
||||||
"mappingsCleared": "Mappages de chemin de modèle de base effacés",
|
"mappingsCleared": "Mappages de chemin de modèle de base effacés",
|
||||||
"mappingSaveFailed": "Échec de la sauvegarde des mappages de modèle de base : {message}",
|
"mappingSaveFailed": "Échec de la sauvegarde des mappages de modèle de base : {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של Diffusion Model (UNET) להורדות, ייבוא והעברות",
|
"defaultUnetRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של Diffusion Model (UNET) להורדות, ייבוא והעברות",
|
||||||
"defaultEmbeddingRoot": "תיקיית שורש Embedding",
|
"defaultEmbeddingRoot": "תיקיית שורש Embedding",
|
||||||
"defaultEmbeddingRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של embedding להורדות, ייבוא והעברות",
|
"defaultEmbeddingRootHelp": "הגדר את ספריית השורש המוגדרת כברירת מחדל של embedding להורדות, ייבוא והעברות",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "נתיב אחסון מתכונים",
|
"recipesPath": "נתיב אחסון מתכונים",
|
||||||
"recipesPathHelp": "ספרייה מותאמת אישית אופציונלית למתכונים שנשמרו. השאר ריק כדי להשתמש בתיקיית recipes של שורש LoRA הראשון.",
|
"recipesPathHelp": "ספרייה מותאמת אישית אופציונלית למתכונים שנשמרו. השאר ריק כדי להשתמש בתיקיית recipes של שורש LoRA הראשון.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "טעינת שורשי checkpoint נכשלה: {message}",
|
"checkpointRootsFailed": "טעינת שורשי checkpoint נכשלה: {message}",
|
||||||
"unetRootsFailed": "טעינת שורשי Diffusion Model נכשלה: {message}",
|
"unetRootsFailed": "טעינת שורשי Diffusion Model נכשלה: {message}",
|
||||||
"embeddingRootsFailed": "טעינת שורשי embedding נכשלה: {message}",
|
"embeddingRootsFailed": "טעינת שורשי embedding נכשלה: {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "מיפויי נתיבי מודל בסיס עודכנו ({count})",
|
"mappingsUpdated": "מיפויי נתיבי מודל בסיס עודכנו ({count})",
|
||||||
"mappingsCleared": "מיפויי נתיבי מודל בסיס נוקו",
|
"mappingsCleared": "מיפויי נתיבי מודל בסיס נוקו",
|
||||||
"mappingSaveFailed": "שמירת מיפויי מודל בסיס נכשלה: {message}",
|
"mappingSaveFailed": "שמירת מיפויי מודל בסיס נכשלה: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "ダウンロード、インポート、移動用のデフォルトDiffusion Model (UNET)ルートディレクトリを設定",
|
"defaultUnetRootHelp": "ダウンロード、インポート、移動用のデフォルトDiffusion Model (UNET)ルートディレクトリを設定",
|
||||||
"defaultEmbeddingRoot": "Embeddingルート",
|
"defaultEmbeddingRoot": "Embeddingルート",
|
||||||
"defaultEmbeddingRootHelp": "ダウンロード、インポート、移動用のデフォルトembeddingルートディレクトリを設定",
|
"defaultEmbeddingRootHelp": "ダウンロード、インポート、移動用のデフォルトembeddingルートディレクトリを設定",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "レシピ保存先",
|
"recipesPath": "レシピ保存先",
|
||||||
"recipesPathHelp": "保存済みレシピ用の任意のカスタムディレクトリです。空欄にすると最初のLoRAルートのrecipesフォルダーを使用します。",
|
"recipesPathHelp": "保存済みレシピ用の任意のカスタムディレクトリです。空欄にすると最初のLoRAルートのrecipesフォルダーを使用します。",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Checkpointルートの読み込みに失敗しました:{message}",
|
"checkpointRootsFailed": "Checkpointルートの読み込みに失敗しました:{message}",
|
||||||
"unetRootsFailed": "Diffusion Modelルートの読み込みに失敗しました:{message}",
|
"unetRootsFailed": "Diffusion Modelルートの読み込みに失敗しました:{message}",
|
||||||
"embeddingRootsFailed": "embeddingルートの読み込みに失敗しました:{message}",
|
"embeddingRootsFailed": "embeddingルートの読み込みに失敗しました:{message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "ベースモデルパスマッピングが更新されました({count} マッピング)",
|
"mappingsUpdated": "ベースモデルパスマッピングが更新されました({count} マッピング)",
|
||||||
"mappingsCleared": "ベースモデルパスマッピングがクリアされました",
|
"mappingsCleared": "ベースモデルパスマッピングがクリアされました",
|
||||||
"mappingSaveFailed": "ベースモデルマッピングの保存に失敗しました:{message}",
|
"mappingSaveFailed": "ベースモデルマッピングの保存に失敗しました:{message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Diffusion Model (UNET) 루트 디렉토리를 설정합니다",
|
"defaultUnetRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Diffusion Model (UNET) 루트 디렉토리를 설정합니다",
|
||||||
"defaultEmbeddingRoot": "Embedding 루트",
|
"defaultEmbeddingRoot": "Embedding 루트",
|
||||||
"defaultEmbeddingRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Embedding 루트 디렉토리를 설정합니다",
|
"defaultEmbeddingRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Embedding 루트 디렉토리를 설정합니다",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "레시피 저장 경로",
|
"recipesPath": "레시피 저장 경로",
|
||||||
"recipesPathHelp": "저장된 레시피를 위한 선택적 사용자 지정 디렉터리입니다. 비워 두면 첫 번째 LoRA 루트의 recipes 폴더를 사용합니다.",
|
"recipesPathHelp": "저장된 레시피를 위한 선택적 사용자 지정 디렉터리입니다. 비워 두면 첫 번째 LoRA 루트의 recipes 폴더를 사용합니다.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Checkpoint 루트 로딩 실패: {message}",
|
"checkpointRootsFailed": "Checkpoint 루트 로딩 실패: {message}",
|
||||||
"unetRootsFailed": "Diffusion Model 루트 로딩 실패: {message}",
|
"unetRootsFailed": "Diffusion Model 루트 로딩 실패: {message}",
|
||||||
"embeddingRootsFailed": "Embedding 루트 로딩 실패: {message}",
|
"embeddingRootsFailed": "Embedding 루트 로딩 실패: {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "베이스 모델 경로 매핑이 업데이트되었습니다 ({count}개 매핑)",
|
"mappingsUpdated": "베이스 모델 경로 매핑이 업데이트되었습니다 ({count}개 매핑)",
|
||||||
"mappingsCleared": "베이스 모델 경로 매핑이 지워졌습니다",
|
"mappingsCleared": "베이스 모델 경로 매핑이 지워졌습니다",
|
||||||
"mappingSaveFailed": "베이스 모델 매핑 저장 실패: {message}",
|
"mappingSaveFailed": "베이스 모델 매핑 저장 실패: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "Установить корневую папку Diffusion Model (UNET) по умолчанию для загрузок, импорта и перемещений",
|
"defaultUnetRootHelp": "Установить корневую папку Diffusion Model (UNET) по умолчанию для загрузок, импорта и перемещений",
|
||||||
"defaultEmbeddingRoot": "Корневая папка Embedding",
|
"defaultEmbeddingRoot": "Корневая папка Embedding",
|
||||||
"defaultEmbeddingRootHelp": "Установить корневую папку embedding по умолчанию для загрузок, импорта и перемещений",
|
"defaultEmbeddingRootHelp": "Установить корневую папку embedding по умолчанию для загрузок, импорта и перемещений",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "Путь хранения рецептов",
|
"recipesPath": "Путь хранения рецептов",
|
||||||
"recipesPathHelp": "Дополнительный пользовательский каталог для сохранённых рецептов. Оставьте пустым, чтобы использовать папку recipes в первом корне LoRA.",
|
"recipesPathHelp": "Дополнительный пользовательский каталог для сохранённых рецептов. Оставьте пустым, чтобы использовать папку recipes в первом корне LoRA.",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "Не удалось загрузить корни checkpoint: {message}",
|
"checkpointRootsFailed": "Не удалось загрузить корни checkpoint: {message}",
|
||||||
"unetRootsFailed": "Не удалось загрузить корни Diffusion Model: {message}",
|
"unetRootsFailed": "Не удалось загрузить корни Diffusion Model: {message}",
|
||||||
"embeddingRootsFailed": "Не удалось загрузить корни embedding: {message}",
|
"embeddingRootsFailed": "Не удалось загрузить корни embedding: {message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "Сопоставления путей базовых моделей обновлены ({count})",
|
"mappingsUpdated": "Сопоставления путей базовых моделей обновлены ({count})",
|
||||||
"mappingsCleared": "Сопоставления путей базовых моделей очищены",
|
"mappingsCleared": "Сопоставления путей базовых моделей очищены",
|
||||||
"mappingSaveFailed": "Не удалось сохранить сопоставления базовых моделей: {message}",
|
"mappingSaveFailed": "Не удалось сохранить сопоставления базовых моделей: {message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "设置下载、导入和移动时的默认 Diffusion Model (UNET) 根目录",
|
"defaultUnetRootHelp": "设置下载、导入和移动时的默认 Diffusion Model (UNET) 根目录",
|
||||||
"defaultEmbeddingRoot": "Embedding 根目录",
|
"defaultEmbeddingRoot": "Embedding 根目录",
|
||||||
"defaultEmbeddingRootHelp": "设置下载、导入和移动时的默认 Embedding 根目录",
|
"defaultEmbeddingRootHelp": "设置下载、导入和移动时的默认 Embedding 根目录",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "配方存储路径",
|
"recipesPath": "配方存储路径",
|
||||||
"recipesPathHelp": "已保存配方的可选自定义目录。留空则使用第一个 LoRA 根目录下的 recipes 文件夹。",
|
"recipesPathHelp": "已保存配方的可选自定义目录。留空则使用第一个 LoRA 根目录下的 recipes 文件夹。",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "加载 Checkpoint 根目录失败:{message}",
|
"checkpointRootsFailed": "加载 Checkpoint 根目录失败:{message}",
|
||||||
"unetRootsFailed": "加载 Diffusion Model 根目录失败:{message}",
|
"unetRootsFailed": "加载 Diffusion Model 根目录失败:{message}",
|
||||||
"embeddingRootsFailed": "加载 Embedding 根目录失败:{message}",
|
"embeddingRootsFailed": "加载 Embedding 根目录失败:{message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "基础模型路径映射已更新({count} 条映射)",
|
"mappingsUpdated": "基础模型路径映射已更新({count} 条映射)",
|
||||||
"mappingsCleared": "基础模型路径映射已清除",
|
"mappingsCleared": "基础模型路径映射已清除",
|
||||||
"mappingSaveFailed": "保存基础模型映射失败:{message}",
|
"mappingSaveFailed": "保存基础模型映射失败:{message}",
|
||||||
|
|||||||
@@ -535,6 +535,16 @@
|
|||||||
"defaultUnetRootHelp": "設定下載、匯入和移動時的預設 Diffusion Model (UNET) 根目錄",
|
"defaultUnetRootHelp": "設定下載、匯入和移動時的預設 Diffusion Model (UNET) 根目錄",
|
||||||
"defaultEmbeddingRoot": "Embedding 根目錄",
|
"defaultEmbeddingRoot": "Embedding 根目錄",
|
||||||
"defaultEmbeddingRootHelp": "設定下載、匯入和移動時的預設 Embedding 根目錄",
|
"defaultEmbeddingRootHelp": "設定下載、匯入和移動時的預設 Embedding 根目錄",
|
||||||
|
"defaultVaeRoot": "[TODO: Translate] VAE Root",
|
||||||
|
"defaultVaeRootHelp": "[TODO: Translate] Set default VAE root directory for downloads, imports and moves",
|
||||||
|
"defaultUpscalerRoot": "[TODO: Translate] Upscaler Root",
|
||||||
|
"defaultUpscalerRootHelp": "[TODO: Translate] Set default upscaler root directory for downloads, imports and moves",
|
||||||
|
"defaultTextEncoderRoot": "[TODO: Translate] Text Encoder Root",
|
||||||
|
"defaultTextEncoderRootHelp": "[TODO: Translate] Set default text encoder root directory for downloads, imports and moves",
|
||||||
|
"defaultClipVisionRoot": "[TODO: Translate] CLIP Vision Root",
|
||||||
|
"defaultClipVisionRootHelp": "[TODO: Translate] Set default CLIP vision root directory for downloads, imports and moves",
|
||||||
|
"defaultControlnetRoot": "[TODO: Translate] ControlNet Root",
|
||||||
|
"defaultControlnetRootHelp": "[TODO: Translate] Set default ControlNet root directory for downloads, imports and moves",
|
||||||
"recipesPath": "配方儲存路徑",
|
"recipesPath": "配方儲存路徑",
|
||||||
"recipesPathHelp": "已儲存配方的可選自訂目錄。留空則使用第一個 LoRA 根目錄下的 recipes 資料夾。",
|
"recipesPathHelp": "已儲存配方的可選自訂目錄。留空則使用第一個 LoRA 根目錄下的 recipes 資料夾。",
|
||||||
"recipesPathPlaceholder": "/path/to/recipes",
|
"recipesPathPlaceholder": "/path/to/recipes",
|
||||||
@@ -2342,6 +2352,7 @@
|
|||||||
"checkpointRootsFailed": "載入 checkpoint 根目錄失敗:{message}",
|
"checkpointRootsFailed": "載入 checkpoint 根目錄失敗:{message}",
|
||||||
"unetRootsFailed": "載入 Diffusion Model 根目錄失敗:{message}",
|
"unetRootsFailed": "載入 Diffusion Model 根目錄失敗:{message}",
|
||||||
"embeddingRootsFailed": "載入 embedding 根目錄失敗:{message}",
|
"embeddingRootsFailed": "載入 embedding 根目錄失敗:{message}",
|
||||||
|
"otherRootsFailed": "[TODO: Translate] Failed to load other model roots: {message}",
|
||||||
"mappingsUpdated": "基礎模型路徑對應已更新({count} 個對應)",
|
"mappingsUpdated": "基礎模型路徑對應已更新({count} 個對應)",
|
||||||
"mappingsCleared": "基礎模型路徑對應已清除",
|
"mappingsCleared": "基礎模型路徑對應已清除",
|
||||||
"mappingSaveFailed": "儲存基礎模型對應失敗:{message}",
|
"mappingSaveFailed": "儲存基礎模型對應失敗:{message}",
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ export const MODEL_SPECIFIC_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
[MODEL_TYPES.OTHER]: {
|
[MODEL_TYPES.OTHER]: {
|
||||||
metadata: `/api/lm/${MODEL_TYPES.OTHER}/metadata`,
|
metadata: `/api/lm/${MODEL_TYPES.OTHER}/metadata`,
|
||||||
|
roots_by_subtype: `/api/lm/${MODEL_TYPES.OTHER}/roots_by_subtype`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,42 @@ import { BaseModelApiClient } from './baseModelApi.js';
|
|||||||
* Other-models-specific API client (VAE, upscalers, text encoders, etc.)
|
* Other-models-specific API client (VAE, upscalers, text encoders, etc.)
|
||||||
*/
|
*/
|
||||||
export class OtherApiClient extends BaseModelApiClient {
|
export class OtherApiClient extends BaseModelApiClient {
|
||||||
|
/**
|
||||||
|
* Get other-model roots, optionally narrowed to one sub_type
|
||||||
|
* (vae/upscaler/text_encoder/clip_vision/controlnet).
|
||||||
|
*
|
||||||
|
* Without a sub_type this falls back to the merged roots list
|
||||||
|
* (GET /api/lm/other/roots); with one it reads the grouped
|
||||||
|
* roots_by_subtype map and extracts the matching list.
|
||||||
|
*/
|
||||||
|
async fetchModelRoots(subType = null) {
|
||||||
|
if (!subType) {
|
||||||
|
return super.fetchModelRoots();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.fetchRootsBySubType();
|
||||||
|
const groupedRoots = data.roots_by_subtype || {};
|
||||||
|
return {
|
||||||
|
success: data.success !== false,
|
||||||
|
roots: groupedRoots[subType] || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get other-model roots grouped by sub_type.
|
||||||
|
* GET /api/lm/other/roots_by_subtype
|
||||||
|
* -> { success, roots_by_subtype: {sub_type: [...]} }
|
||||||
|
*/
|
||||||
|
async fetchRootsBySubType() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.specific.roots_by_subtype);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch other-model roots by sub_type');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching other-model roots by sub_type:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { PageControls } from './PageControls.js';
|
import { PageControls } from './PageControls.js';
|
||||||
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
|
import { downloadManager } from '../../managers/DownloadManager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OtherControls class - Extends PageControls for the Other Models page
|
* OtherControls class - Extends PageControls for the Other Models page
|
||||||
@@ -39,6 +40,11 @@ export class OtherControls extends PageControls {
|
|||||||
return await getModelApiClient().fetchCivitaiMetadata();
|
return await getModelApiClient().fetchCivitaiMetadata();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Add show download modal functionality
|
||||||
|
showDownloadModal: () => {
|
||||||
|
downloadManager.showDownloadModal();
|
||||||
|
},
|
||||||
|
|
||||||
toggleBulkMode: () => {
|
toggleBulkMode: () => {
|
||||||
if (window.bulkManager) {
|
if (window.bulkManager) {
|
||||||
window.bulkManager.toggleBulkMode();
|
window.bulkManager.toggleBulkMode();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { isModelWeightFile } from '../utils/modelFileTypes.js';
|
|||||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { FolderTreeManager } from '../components/FolderTreeManager.js';
|
import { FolderTreeManager } from '../components/FolderTreeManager.js';
|
||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
|
import { MODEL_SUBTYPE_DISPLAY_NAMES } from '../utils/constants.js';
|
||||||
import { buildCivitaiUrl, extractCivitaiModelUrlParts, normalizeCivitaiPageHost } from '../utils/civitaiUtils.js';
|
import { buildCivitaiUrl, extractCivitaiModelUrlParts, normalizeCivitaiPageHost } from '../utils/civitaiUtils.js';
|
||||||
import { formatFileSize } from '../utils/formatters.js';
|
import { formatFileSize } from '../utils/formatters.js';
|
||||||
import { showDownloadBatchSummary } from '../components/DownloadBatchSummaryModal.js';
|
import { showDownloadBatchSummary } from '../components/DownloadBatchSummaryModal.js';
|
||||||
@@ -956,11 +957,17 @@ export class DownloadManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
this._isDiffusionModel = await this._resolveIsDiffusionModel();
|
||||||
|
this._otherSubType = await this._resolveOtherSubType();
|
||||||
|
|
||||||
let rootsData;
|
let rootsData;
|
||||||
if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') {
|
if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') {
|
||||||
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
rootsData = await this.apiClient.fetchModelRoots('diffusion_model');
|
||||||
|
} else if (this.apiClient.modelType === 'other' && this._otherSubType) {
|
||||||
|
rootsData = await this.apiClient.fetchModelRoots(this._otherSubType);
|
||||||
} else {
|
} else {
|
||||||
|
// An undecidable other sub_type (null) intentionally lands
|
||||||
|
// here: fetchModelRoots() lists all other roots so the user
|
||||||
|
// can pick manually.
|
||||||
rootsData = await this.apiClient.fetchModelRoots();
|
rootsData = await this.apiClient.fetchModelRoots();
|
||||||
}
|
}
|
||||||
const modelRoot = document.getElementById('modelRoot');
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
@@ -968,19 +975,29 @@ export class DownloadManager {
|
|||||||
`<option value="${root}">${root}</option>`
|
`<option value="${root}">${root}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const singularType = this._isDiffusionModel
|
let defaultRoot;
|
||||||
? 'unet'
|
let subtypeDisplay;
|
||||||
: this.apiClient.modelType.replace(/s$/, '');
|
if (this.apiClient.modelType === 'other') {
|
||||||
const defaultRootKey = `default_${singularType}_root`;
|
const otherDefaultRoots = state.global.settings.default_other_roots || {};
|
||||||
const defaultRoot = state.global.settings[defaultRootKey];
|
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
|
||||||
console.log(`Default root for ${singularType}:`, defaultRoot);
|
subtypeDisplay = this._otherSubType
|
||||||
|
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
|
||||||
|
: this.apiClient.apiConfig.config.displayName;
|
||||||
|
} else {
|
||||||
|
const singularType = this._isDiffusionModel
|
||||||
|
? 'unet'
|
||||||
|
: this.apiClient.modelType.replace(/s$/, '');
|
||||||
|
const defaultRootKey = `default_${singularType}_root`;
|
||||||
|
defaultRoot = state.global.settings[defaultRootKey];
|
||||||
|
subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
||||||
|
}
|
||||||
|
console.log('Default root:', defaultRoot);
|
||||||
console.log('Available roots:', rootsData.roots);
|
console.log('Available roots:', rootsData.roots);
|
||||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||||
console.log(`Setting default root: ${defaultRoot}`);
|
console.log(`Setting default root: ${defaultRoot}`);
|
||||||
modelRoot.value = defaultRoot;
|
modelRoot.value = defaultRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName;
|
|
||||||
document.getElementById('modelRootLabel').textContent =
|
document.getElementById('modelRootLabel').textContent =
|
||||||
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
translate('modals.download.selectTypeRoot', { type: subtypeDisplay });
|
||||||
|
|
||||||
@@ -1065,6 +1082,50 @@ export class DownloadManager {
|
|||||||
return localFileTypeCheck;
|
return localFileTypeCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve which other-page sub_type (vae/upscaler/text_encoder/
|
||||||
|
* clip_vision/controlnet) this download routes to. The backend owns the
|
||||||
|
* routing rule (explicit file pick first, model.type next, file.type
|
||||||
|
* fallback), so the location step sends both the picked file's type
|
||||||
|
* (selected_file_type) and the version's full file-type list and lets
|
||||||
|
* the backend apply its priority chain. Returns null when the sub_type
|
||||||
|
* cannot be decided; the location step then lists all other roots for
|
||||||
|
* manual selection instead of guessing a folder.
|
||||||
|
*/
|
||||||
|
async _resolveOtherSubType() {
|
||||||
|
// Only other-page downloads route by sub_type; without version
|
||||||
|
// metadata (e.g. Hugging Face downloads) there is nothing to route on.
|
||||||
|
if (this.apiClient.modelType !== 'other'
|
||||||
|
|| (!this.selectedFile && !this.currentVersion)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileTypes = (this.currentVersion?.files || []).map(f => f.type);
|
||||||
|
const response = await fetch(DOWNLOAD_ENDPOINTS.routing, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_type: 'other',
|
||||||
|
base_model: this.currentVersion?.baseModel || '',
|
||||||
|
file_types: fileTypes,
|
||||||
|
...(this.selectedFile
|
||||||
|
? { selected_file_type: this.selectedFile.type }
|
||||||
|
: {}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`routing endpoint returned ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data.sub_type || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[download] other routing endpoint unavailable, '
|
||||||
|
+ 'falling back to manual root selection:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadDefaultPathSetting() {
|
loadDefaultPathSetting() {
|
||||||
const modelType = this.apiClient.modelType;
|
const modelType = this.apiClient.modelType;
|
||||||
const storageKey = `use_default_path_${modelType}`;
|
const storageKey = `use_default_path_${modelType}`;
|
||||||
|
|||||||
@@ -1153,6 +1153,9 @@ export class SettingsManager {
|
|||||||
// Load default unet root
|
// Load default unet root
|
||||||
await this.loadUnetRoots();
|
await this.loadUnetRoots();
|
||||||
|
|
||||||
|
// Load default other-model roots (per sub_type)
|
||||||
|
await this.loadOtherRoots();
|
||||||
|
|
||||||
// Load extra folder paths
|
// Load extra folder paths
|
||||||
this.loadExtraFolderPaths();
|
this.loadExtraFolderPaths();
|
||||||
|
|
||||||
@@ -1658,6 +1661,51 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadOtherRoots() {
|
||||||
|
const selects = document.querySelectorAll('select[data-other-root-subtype]');
|
||||||
|
if (!selects.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch other-model roots grouped by sub_type
|
||||||
|
const response = await fetch('/api/lm/other/roots_by_subtype');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch other model roots');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const groupedRoots = data.roots_by_subtype || {};
|
||||||
|
const defaultRoots = state.global.settings.default_other_roots || {};
|
||||||
|
|
||||||
|
selects.forEach((select) => {
|
||||||
|
const subType = select.dataset.otherRootSubtype;
|
||||||
|
const roots = groupedRoots[subType] || [];
|
||||||
|
if (!roots.length) {
|
||||||
|
this.showNoRootsPlaceholder(select);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.innerHTML = '';
|
||||||
|
select.disabled = false;
|
||||||
|
|
||||||
|
// Add options for each root
|
||||||
|
roots.forEach(root => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = root;
|
||||||
|
option.textContent = root;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultRoot = defaultRoots[subType] || '';
|
||||||
|
select.value = roots.includes(defaultRoot) ? defaultRoot : roots[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading other model roots:', error);
|
||||||
|
selects.forEach((select) => this.showNoRootsPlaceholder(select));
|
||||||
|
showToast('toast.settings.otherRootsFailed', { message: error.message }, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadEmbeddingRoots() {
|
async loadEmbeddingRoots() {
|
||||||
const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot');
|
const defaultEmbeddingRootSelect = document.getElementById('defaultEmbeddingRoot');
|
||||||
if (!defaultEmbeddingRootSelect) return;
|
if (!defaultEmbeddingRootSelect) return;
|
||||||
@@ -2339,6 +2387,27 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save one sub_type entry of the default_other_roots dict setting
|
||||||
|
* (read-modify-write: the backend stores the whole mapping).
|
||||||
|
*/
|
||||||
|
async saveOtherRootSetting(subType, value) {
|
||||||
|
try {
|
||||||
|
const defaultRoots = { ...(state.global.settings.default_other_roots || {}) };
|
||||||
|
if (value) {
|
||||||
|
defaultRoots[subType] = value;
|
||||||
|
} else {
|
||||||
|
delete defaultRoots[subType];
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveSetting('default_other_roots', defaultRoots);
|
||||||
|
|
||||||
|
showToast('toast.settings.settingsUpdated', { setting: `default ${subType} root` }, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the recipes page layout (grid | masonry) and rebuild the scroller.
|
* Save the recipes page layout (grid | masonry) and rebuild the scroller.
|
||||||
* Shared entry point for the settings modal segmented control and the
|
* Shared entry point for the settings modal segmented control and the
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
|||||||
default_lora_root: '',
|
default_lora_root: '',
|
||||||
default_checkpoint_root: '',
|
default_checkpoint_root: '',
|
||||||
default_embedding_root: '',
|
default_embedding_root: '',
|
||||||
|
default_other_roots: {},
|
||||||
recipes_path: '',
|
recipes_path: '',
|
||||||
base_model_path_mappings: {},
|
base_model_path_mappings: {},
|
||||||
download_path_templates: {},
|
download_path_templates: {},
|
||||||
@@ -72,6 +73,7 @@ export function createDefaultSettings() {
|
|||||||
base_model_path_mappings: {},
|
base_model_path_mappings: {},
|
||||||
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
||||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||||
|
default_other_roots: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,14 +80,12 @@
|
|||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span><span>{{ t('loras.controls.fetch.action') }}</span> <kbd class="shortcut-key">F</kbd></span></button>
|
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span><span>{{ t('loras.controls.fetch.action') }}</span> <kbd class="shortcut-key">F</kbd></span></button>
|
||||||
</div>
|
</div>
|
||||||
{% if page_id != 'other' %}
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
||||||
<i class="fas fa-cloud-download-alt"></i> <span><span>{{ t('loras.controls.download.action') }}</span> <kbd class="shortcut-key">D</kbd></span>
|
<i class="fas fa-cloud-download-alt"></i> <span><span>{{ t('loras.controls.download.action') }}</span> <kbd class="shortcut-key">D</kbd></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if page_id == 'recipes' %}
|
{% if page_id == 'recipes' %}
|
||||||
<div title="{{ t('recipes.controls.import.title') }}" class="control-group">
|
<div title="{{ t('recipes.controls.import.title') }}" class="control-group">
|
||||||
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ t('recipes.controls.import.action') }}</button>
|
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ t('recipes.controls.import.action') }}</button>
|
||||||
|
|||||||
@@ -38,6 +38,34 @@
|
|||||||
{{ sm.setting_select('defaultUnetRoot', 'default_unet_root', 'settings.folderSettings.defaultUnetRoot', [], 'settings.folderSettings.defaultUnetRootHelp') }}
|
{{ sm.setting_select('defaultUnetRoot', 'default_unet_root', 'settings.folderSettings.defaultUnetRoot', [], 'settings.folderSettings.defaultUnetRootHelp') }}
|
||||||
|
|
||||||
{{ sm.setting_select('defaultEmbeddingRoot', 'default_embedding_root', 'settings.folderSettings.defaultEmbeddingRoot', [], 'settings.folderSettings.defaultEmbeddingRootHelp') }}
|
{{ sm.setting_select('defaultEmbeddingRoot', 'default_embedding_root', 'settings.folderSettings.defaultEmbeddingRoot', [], 'settings.folderSettings.defaultEmbeddingRootHelp') }}
|
||||||
|
|
||||||
|
{% set other_root_selects = [
|
||||||
|
('vae', 'defaultOtherRootVae', 'settings.folderSettings.defaultVaeRoot', 'settings.folderSettings.defaultVaeRootHelp'),
|
||||||
|
('upscaler', 'defaultOtherRootUpscaler', 'settings.folderSettings.defaultUpscalerRoot', 'settings.folderSettings.defaultUpscalerRootHelp'),
|
||||||
|
('text_encoder', 'defaultOtherRootTextEncoder', 'settings.folderSettings.defaultTextEncoderRoot', 'settings.folderSettings.defaultTextEncoderRootHelp'),
|
||||||
|
('clip_vision', 'defaultOtherRootClipVision', 'settings.folderSettings.defaultClipVisionRoot', 'settings.folderSettings.defaultClipVisionRootHelp'),
|
||||||
|
] %}
|
||||||
|
{% if 'controlnet' in (settings.get('enabled_other_folders') or []) %}
|
||||||
|
{% set other_root_selects = other_root_selects + [
|
||||||
|
('controlnet', 'defaultOtherRootControlnet', 'settings.folderSettings.defaultControlnetRoot', 'settings.folderSettings.defaultControlnetRootHelp'),
|
||||||
|
] %}
|
||||||
|
{% endif %}
|
||||||
|
{% for sub_type, select_id, label_key, help_key in other_root_selects %}
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="{{ select_id }}">
|
||||||
|
{{ t(label_key) }}
|
||||||
|
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t(help_key) }}"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="{{ select_id }}" data-other-root-subtype="{{ sub_type }}" onchange="settingsManager.saveOtherRootSetting('{{ sub_type }}', this.value)">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recipe Settings -->
|
<!-- Recipe Settings -->
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ vi.mock(SUMMARY_MODULE, () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE);
|
const { DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE);
|
||||||
|
const { state } = await import(STATE_MODULE);
|
||||||
|
|
||||||
describe('DownloadManager._resolveIsDiffusionModel', () => {
|
describe('DownloadManager._resolveIsDiffusionModel', () => {
|
||||||
let manager;
|
let manager;
|
||||||
@@ -144,3 +145,185 @@ describe('DownloadManager._resolveIsDiffusionModel', () => {
|
|||||||
expect(fetchMock).not.toHaveBeenCalled();
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DownloadManager._resolveOtherSubType', () => {
|
||||||
|
let manager;
|
||||||
|
let fetchMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
manager = new DownloadManager();
|
||||||
|
manager.apiClient = { modelType: 'other' };
|
||||||
|
manager.selectedFile = null;
|
||||||
|
manager.selectedFiles = [];
|
||||||
|
manager.currentVersion = null;
|
||||||
|
fetchMock = vi.fn();
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
function mockRoutingResponse(data, ok = true) {
|
||||||
|
fetchMock.mockResolvedValue({
|
||||||
|
ok,
|
||||||
|
status: ok ? 200 : 500,
|
||||||
|
json: async () => data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('returns the backend sub_type for other downloads', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'Flux.1 D', files: [{ type: 'VAE' }] };
|
||||||
|
mockRoutingResponse({ success: true, root_kind: 'other', sub_type: 'vae' });
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBe('vae');
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith('/api/lm/download/routing', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_type: 'other',
|
||||||
|
base_model: 'Flux.1 D',
|
||||||
|
file_types: ['VAE'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends selected_file_type plus all version file types when a file is selected', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'Flux.1 D', files: [{ type: 'Model' }, { type: 'VAE' }] };
|
||||||
|
manager.selectedFile = { type: 'VAE' };
|
||||||
|
mockRoutingResponse({ success: true, root_kind: 'other', sub_type: 'vae' });
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBe('vae');
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
||||||
|
expect(body.selected_file_type).toBe('VAE');
|
||||||
|
expect(body.file_types).toEqual(['Model', 'VAE']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits selected_file_type when no file is selected', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'Flux.1 D', files: [{ type: 'Model' }, { type: 'VAE' }] };
|
||||||
|
mockRoutingResponse({ success: true, root_kind: 'other', sub_type: 'vae' });
|
||||||
|
|
||||||
|
await manager._resolveOtherSubType();
|
||||||
|
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
||||||
|
expect('selected_file_type' in body).toBe(false);
|
||||||
|
expect(body.file_types).toEqual(['Model', 'VAE']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when the backend cannot decide a sub_type', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'SDXL 1.0', files: [{ type: 'Model' }] };
|
||||||
|
mockRoutingResponse({ success: true, root_kind: 'other', sub_type: null });
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when the endpoint fails', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'SDXL 1.0', files: [{ type: 'VAE' }] };
|
||||||
|
fetchMock.mockRejectedValue(new Error('network down'));
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null on a non-ok response', async () => {
|
||||||
|
manager.currentVersion = { baseModel: 'SDXL 1.0', files: [{ type: 'VAE' }] };
|
||||||
|
mockRoutingResponse({}, false);
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never calls the endpoint for non-other pages', async () => {
|
||||||
|
manager.apiClient = { modelType: 'loras' };
|
||||||
|
manager.currentVersion = { baseModel: 'SDXL 1.0', files: [{ type: 'VAE' }] };
|
||||||
|
|
||||||
|
expect(await manager._resolveOtherSubType()).toBeNull();
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never calls the endpoint without version metadata (e.g. Hugging Face)', async () => {
|
||||||
|
expect(await manager._resolveOtherSubType()).toBeNull();
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DownloadManager.proceedToLocationContent (other page)', () => {
|
||||||
|
let manager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<select id="modelRoot"></select>
|
||||||
|
<label id="modelRootLabel"></label>
|
||||||
|
<input id="folderPath" />
|
||||||
|
`;
|
||||||
|
state.global.settings = {};
|
||||||
|
|
||||||
|
manager = new DownloadManager();
|
||||||
|
manager.apiClient = {
|
||||||
|
modelType: 'other',
|
||||||
|
apiConfig: { config: { displayName: 'Other Model' } },
|
||||||
|
fetchModelRoots: vi.fn(),
|
||||||
|
};
|
||||||
|
manager.selectedFile = null;
|
||||||
|
manager.selectedFiles = [];
|
||||||
|
manager.currentVersion = { baseModel: 'Flux.1 D', files: [{ type: 'VAE' }] };
|
||||||
|
manager.initializeFolderTree = vi.fn().mockResolvedValue();
|
||||||
|
manager.folderTreeManager = { init: vi.fn() };
|
||||||
|
manager.loadDefaultPathSetting = vi.fn();
|
||||||
|
manager.updateTargetPath = vi.fn();
|
||||||
|
vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches sub_type roots and preselects the configured default root', async () => {
|
||||||
|
vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue('vae');
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/vae-a', '/models/vae-b'],
|
||||||
|
});
|
||||||
|
state.global.settings.default_other_roots = { vae: '/models/vae-b' };
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith('vae');
|
||||||
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
|
expect(Array.from(modelRoot.options).map(o => o.value)).toEqual([
|
||||||
|
'/models/vae-a',
|
||||||
|
'/models/vae-b',
|
||||||
|
]);
|
||||||
|
expect(modelRoot.value).toBe('/models/vae-b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists all other roots for manual selection when the sub_type is undecidable', async () => {
|
||||||
|
vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue(null);
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/vae', '/models/upscale'],
|
||||||
|
});
|
||||||
|
state.global.settings.default_other_roots = { vae: '/models/upscale' };
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
// No argument: the merged /api/lm/other/roots list
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith();
|
||||||
|
const modelRoot = document.getElementById('modelRoot');
|
||||||
|
expect(Array.from(modelRoot.options).map(o => o.value)).toEqual([
|
||||||
|
'/models/vae',
|
||||||
|
'/models/upscale',
|
||||||
|
]);
|
||||||
|
// Without a resolved sub_type no default_other_roots entry applies,
|
||||||
|
// so the first option stays selected even though a vae default exists.
|
||||||
|
expect(modelRoot.value).toBe('/models/vae');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the first root selected when no default is configured for the sub_type', async () => {
|
||||||
|
vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue('upscaler');
|
||||||
|
manager.apiClient.fetchModelRoots.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
roots: ['/models/upscale'],
|
||||||
|
});
|
||||||
|
state.global.settings.default_other_roots = { vae: '/models/vae-a' };
|
||||||
|
|
||||||
|
await manager.proceedToLocationContent();
|
||||||
|
|
||||||
|
expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith('upscaler');
|
||||||
|
expect(document.getElementById('modelRoot').value).toBe('/models/upscale');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ vi.mock('../../../static/js/state/index.js', () => {
|
|||||||
},
|
},
|
||||||
createDefaultSettings: () => ({
|
createDefaultSettings: () => ({
|
||||||
language: 'en',
|
language: 'en',
|
||||||
|
default_other_roots: {},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -502,6 +503,182 @@ describe('SettingsManager library controls', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('SettingsManager other-model root selects', () => {
|
||||||
|
const appendOtherRootSelects = (...subTypes) => {
|
||||||
|
const selects = {};
|
||||||
|
subTypes.forEach((subType) => {
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.dataset.otherRootSubtype = subType;
|
||||||
|
document.body.appendChild(select);
|
||||||
|
selects[subType] = select;
|
||||||
|
});
|
||||||
|
return selects;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('loadOtherRoots', () => {
|
||||||
|
it('populates each sub_type select from the grouped roots and preselects defaults', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
const selects = appendOtherRootSelects('vae', 'upscaler');
|
||||||
|
selects.vae.disabled = true;
|
||||||
|
|
||||||
|
state.global.settings = {
|
||||||
|
default_other_roots: { vae: '/models/vae-b' },
|
||||||
|
};
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
success: true,
|
||||||
|
roots_by_subtype: {
|
||||||
|
vae: ['/models/vae-a', '/models/vae-b'],
|
||||||
|
upscaler: ['/models/upscale'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.loadOtherRoots();
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/lm/other/roots_by_subtype');
|
||||||
|
expect(Array.from(selects.vae.options).map(o => o.value)).toEqual([
|
||||||
|
'/models/vae-a',
|
||||||
|
'/models/vae-b',
|
||||||
|
]);
|
||||||
|
expect(selects.vae.value).toBe('/models/vae-b');
|
||||||
|
expect(selects.vae.disabled).toBe(false);
|
||||||
|
expect(Array.from(selects.upscaler.options).map(o => o.value)).toEqual([
|
||||||
|
'/models/upscale',
|
||||||
|
]);
|
||||||
|
// No configured default: first root wins
|
||||||
|
expect(selects.upscaler.value).toBe('/models/upscale');
|
||||||
|
expect(showToast).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a placeholder on selects whose sub_type has no roots', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
const selects = appendOtherRootSelects('vae', 'controlnet');
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
success: true,
|
||||||
|
roots_by_subtype: { vae: ['/models/vae-a'] },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.loadOtherRoots();
|
||||||
|
|
||||||
|
expect(selects.controlnet.options).toHaveLength(1);
|
||||||
|
expect(selects.controlnet.options[0].value).toBe('');
|
||||||
|
expect(selects.controlnet.options[0].textContent).toBe('No Default');
|
||||||
|
expect(selects.controlnet.disabled).toBe(true);
|
||||||
|
expect(selects.vae.disabled).toBe(false);
|
||||||
|
expect(showToast).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error toast and placeholders when the request fails', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
const selects = appendOtherRootSelects('vae', 'upscaler');
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.loadOtherRoots();
|
||||||
|
|
||||||
|
expect(selects.vae.disabled).toBe(true);
|
||||||
|
expect(selects.upscaler.disabled).toBe(true);
|
||||||
|
expect(showToast).toHaveBeenCalledWith(
|
||||||
|
'toast.settings.otherRootsFailed',
|
||||||
|
expect.objectContaining({ message: expect.any(String) }),
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call the API when no sub_type selects exist', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
global.fetch = vi.fn();
|
||||||
|
|
||||||
|
await manager.loadOtherRoots();
|
||||||
|
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveOtherRootSetting', () => {
|
||||||
|
it('read-modify-writes the default_other_roots dict and posts it whole', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
state.global.settings = {
|
||||||
|
default_other_roots: { vae: '/models/vae-a' },
|
||||||
|
};
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.saveOtherRootSetting('upscaler', '/models/upscale');
|
||||||
|
|
||||||
|
expect(state.global.settings.default_other_roots).toEqual({
|
||||||
|
vae: '/models/vae-a',
|
||||||
|
upscaler: '/models/upscale',
|
||||||
|
});
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/lm/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
default_other_roots: {
|
||||||
|
vae: '/models/vae-a',
|
||||||
|
upscaler: '/models/upscale',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(showToast).toHaveBeenCalledWith(
|
||||||
|
'toast.settings.settingsUpdated',
|
||||||
|
expect.objectContaining({ setting: expect.any(String) }),
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the sub_type key when the value is empty', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
state.global.settings = {
|
||||||
|
default_other_roots: { vae: '/models/vae-a' },
|
||||||
|
};
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.saveOtherRootSetting('vae', '');
|
||||||
|
|
||||||
|
expect(state.global.settings.default_other_roots).toEqual({});
|
||||||
|
expect(JSON.parse(global.fetch.mock.calls[0][1].body)).toEqual({
|
||||||
|
default_other_roots: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error toast when the backend save fails', async () => {
|
||||||
|
const manager = createManager();
|
||||||
|
state.global.settings = { default_other_roots: {} };
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.saveOtherRootSetting('vae', '/models/vae-a');
|
||||||
|
|
||||||
|
expect(showToast).toHaveBeenCalledWith(
|
||||||
|
'toast.settings.settingSaveFailed',
|
||||||
|
expect.objectContaining({ message: expect.any(String) }),
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('SettingsManager recipes layout switch', () => {
|
describe('SettingsManager recipes layout switch', () => {
|
||||||
it('dispatches lm:recipes-layout-changed without recalculating the old scroller', async () => {
|
it('dispatches lm:recipes-layout-changed without recalculating the old scroller', async () => {
|
||||||
const manager = createManager();
|
const manager = createManager();
|
||||||
|
|||||||
Reference in New Issue
Block a user