feat(frontend): enable downloads on Other page and default_other_roots settings UI

This commit is contained in:
Will Miao
2026-09-12 15:56:47 +08:00
parent f2a7297cb9
commit fc3b2d7c13
20 changed files with 682 additions and 9 deletions
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+11
View File
@@ -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}",
+1
View File
@@ -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`,
} }
}; };
+38
View File
@@ -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();
+64 -3
View File
@@ -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('');
let defaultRoot;
let subtypeDisplay;
if (this.apiClient.modelType === 'other') {
const otherDefaultRoots = state.global.settings.default_other_roots || {};
defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : '';
subtypeDisplay = this._otherSubType
? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType)
: this.apiClient.apiConfig.config.displayName;
} else {
const singularType = this._isDiffusionModel const singularType = this._isDiffusionModel
? 'unet' ? 'unet'
: this.apiClient.modelType.replace(/s$/, ''); : this.apiClient.modelType.replace(/s$/, '');
const defaultRootKey = `default_${singularType}_root`; const defaultRootKey = `default_${singularType}_root`;
const defaultRoot = state.global.settings[defaultRootKey]; defaultRoot = state.global.settings[defaultRootKey];
console.log(`Default root for ${singularType}:`, defaultRoot); 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}`;
+69
View File
@@ -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
+2
View File
@@ -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: {},
}; };
} }
-2
View File
@@ -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();