mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
Compare commits
6 Commits
v1.2.3
...
bf497d5144
| Author | SHA1 | Date | |
|---|---|---|---|
| bf497d5144 | |||
| 369613f811 | |||
| 9eeebac40b | |||
| b9a516c9f8 | |||
| ef7fa7d3dd | |||
| 9c67dbbf15 |
@@ -57,6 +57,15 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL).
|
|||||||
> prototyped and removed because it collided with the browser's Alt + Arrow handling and the
|
> prototyped and removed because it collided with the browser's Alt + Arrow handling and the
|
||||||
> modal's arrow-key navigation.
|
> modal's arrow-key navigation.
|
||||||
|
|
||||||
|
> **Status (2026-09, standalone no-paths guidance):** the standalone branch of the
|
||||||
|
> `other.noPaths` empty state now shows the real `settings.json` path plus an
|
||||||
|
> `other.noPaths.openSettingsFolder` button (each locale reuses its
|
||||||
|
> `settings.openSettingsFileLocation.label` rendering), and `descriptionStandalone` was
|
||||||
|
> reworded in `en.json` — from "none of the configured folders exist on disk" to "no
|
||||||
|
> other-model folders were found; add the folder keys you need to the `folder_paths`
|
||||||
|
> section" — and re-translated in all 9 locales. The `on disk` phrase now survives only in
|
||||||
|
> the ComfyUI variant (`descriptionComfyUI`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Hard rules (do not violate)
|
## 1. Hard rules (do not violate)
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "Keine Ordner für weitere Modelle gefunden",
|
"title": "Keine Ordner für weitere Modelle gefunden",
|
||||||
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die unten stehenden Ordnerpfade zu settings.json hinzu und starten Sie LoRA Manager neu.",
|
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber es wurden keine Ordner für weitere Modelle gefunden. Fügen Sie die benötigten Ordnerschlüssel zum Abschnitt folder_paths Ihrer settings.json hinzu und starten Sie LoRA Manager neu.",
|
||||||
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.",
|
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.",
|
||||||
"descriptionComfyUI": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die entsprechenden Modellordner zu Ihren ComfyUI-Modellpfaden hinzu und laden Sie diese Seite neu.",
|
"descriptionComfyUI": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die entsprechenden Modellordner zu Ihren ComfyUI-Modellpfaden hinzu und laden Sie diese Seite neu.",
|
||||||
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
|
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
|
||||||
"openSettings": "Einstellungen öffnen"
|
"openSettings": "Einstellungen öffnen",
|
||||||
|
"openSettingsFolder": "Einstellungsordner öffnen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "No other-model folders found",
|
"title": "No other-model folders found",
|
||||||
"descriptionStandalone": "Other Models management is on, but none of the configured model folders exist on disk. Add the folder paths below to settings.json and restart LoRA Manager.",
|
"descriptionStandalone": "Other Models management is on, but no other-model folders were found. Add the folder keys you need to the folder_paths section of your settings.json, then restart LoRA Manager.",
|
||||||
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.",
|
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.",
|
||||||
"descriptionComfyUI": "Other Models management is on, but none of the configured model folders exist on disk. Add the matching model folders to your ComfyUI model paths, then reload this page.",
|
"descriptionComfyUI": "Other Models management is on, but none of the configured model folders exist on disk. Add the matching model folders to your ComfyUI model paths, then reload this page.",
|
||||||
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
|
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
|
||||||
"openSettings": "Open Settings"
|
"openSettings": "Open Settings",
|
||||||
|
"openSettingsFolder": "Open Settings Folder"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "No se encontraron carpetas de otros modelos",
|
"title": "No se encontraron carpetas de otros modelos",
|
||||||
"descriptionStandalone": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las rutas de carpetas de abajo a settings.json y reinicia LoRA Manager.",
|
"descriptionStandalone": "La gestión de otros modelos está activada, pero no se encontraron carpetas de otros modelos. Añade las claves de carpeta que necesites a la sección folder_paths de tu settings.json y reinicia LoRA Manager.",
|
||||||
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.",
|
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.",
|
||||||
"descriptionComfyUI": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las carpetas de modelos correspondientes a tus rutas de modelos de ComfyUI y recarga esta página.",
|
"descriptionComfyUI": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las carpetas de modelos correspondientes a tus rutas de modelos de ComfyUI y recarga esta página.",
|
||||||
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
|
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
|
||||||
"openSettings": "Abrir configuración"
|
"openSettings": "Abrir configuración",
|
||||||
|
"openSettingsFolder": "Abrir carpeta de ajustes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "Aucun dossier d’autres modèles trouvé",
|
"title": "Aucun dossier d’autres modèles trouvé",
|
||||||
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les chemins de dossiers ci-dessous à settings.json, puis redémarrez LoRA Manager.",
|
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun dossier d’autres modèles n’a été trouvé. Ajoutez les clés de dossiers dont vous avez besoin à la section folder_paths de votre settings.json, puis redémarrez LoRA Manager.",
|
||||||
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.",
|
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.",
|
||||||
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.",
|
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés n’existe sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.",
|
||||||
"hintComfyUI": "Les autres modèles sont lus depuis les dossiers vae, upscale_models, text_encoders, clip_vision et controlnet de ComfyUI.",
|
"hintComfyUI": "Les autres modèles sont lus depuis les dossiers vae, upscale_models, text_encoders, clip_vision et controlnet de ComfyUI.",
|
||||||
"openSettings": "Ouvrir les paramètres"
|
"openSettings": "Ouvrir les paramètres",
|
||||||
|
"openSettingsFolder": "Ouvrir le dossier des paramètres"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "לא נמצאו תיקיות של מודלים אחרים",
|
"title": "לא נמצאו תיקיות של מודלים אחרים",
|
||||||
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את נתיבי התיקיות שלמטה ל-settings.json והפעל מחדש את LoRA Manager.",
|
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך לא נמצאו תיקיות של מודלים אחרים. הוסף את מפתחות התיקיות הדרושים למקטע folder_paths ב-settings.json והפעל מחדש את LoRA Manager.",
|
||||||
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
|
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
|
||||||
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
|
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
|
||||||
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
|
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
|
||||||
"openSettings": "פתח הגדרות"
|
"openSettings": "פתח הגדרות",
|
||||||
|
"openSettingsFolder": "פתח תיקיית הגדרות"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "その他のモデルのフォルダーが見つかりません",
|
"title": "その他のモデルのフォルダーが見つかりません",
|
||||||
"descriptionStandalone": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。以下のフォルダーパスをsettings.jsonに追加し、LoRA Managerを再起動してください。",
|
"descriptionStandalone": "その他のモデル管理はオンですが、その他のモデルのフォルダーが見つかりません。必要なフォルダーキーをsettings.jsonのfolder_pathsセクションに追加し、LoRA Managerを再起動してください。",
|
||||||
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
|
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
|
||||||
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
|
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
|
||||||
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
|
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
|
||||||
"openSettings": "設定を開く"
|
"openSettings": "設定を開く",
|
||||||
|
"openSettingsFolder": "設定フォルダーを開く"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "기타 모델 폴더를 찾을 수 없습니다",
|
"title": "기타 모델 폴더를 찾을 수 없습니다",
|
||||||
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 아래 폴더 경로를 settings.json에 추가한 뒤 LoRA Manager를 재시작하세요.",
|
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 기타 모델 폴더를 찾을 수 없습니다. 필요한 폴더 키를 settings.json의 folder_paths 섹션에 추가한 뒤 LoRA Manager를 재시작하세요.",
|
||||||
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
|
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
|
||||||
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
|
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
|
||||||
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
|
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
|
||||||
"openSettings": "설정 열기"
|
"openSettings": "설정 열기",
|
||||||
|
"openSettingsFolder": "설정 폴더 열기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "Папки других моделей не найдены",
|
"title": "Папки других моделей не найдены",
|
||||||
"descriptionStandalone": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте указанные ниже пути к папкам в settings.json и перезапустите LoRA Manager.",
|
"descriptionStandalone": "Управление другими моделями включено, но папки других моделей не найдены. Добавьте нужные ключи папок в раздел folder_paths файла settings.json и перезапустите LoRA Manager.",
|
||||||
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
|
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
|
||||||
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
|
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
|
||||||
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
|
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
|
||||||
"openSettings": "Открыть настройки"
|
"openSettings": "Открыть настройки",
|
||||||
|
"openSettingsFolder": "Открыть папку настроек"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "未找到其他模型文件夹",
|
"title": "未找到其他模型文件夹",
|
||||||
"descriptionStandalone": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将下面的文件夹路径添加到 settings.json,然后重启 LoRA Manager。",
|
"descriptionStandalone": "其他模型管理已开启,但未找到其他模型文件夹。请将你需要的文件夹键添加到 settings.json 的 folder_paths 部分,然后重启 LoRA Manager。",
|
||||||
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
|
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
|
||||||
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
|
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
|
||||||
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
|
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
|
||||||
"openSettings": "打开设置"
|
"openSettings": "打开设置",
|
||||||
|
"openSettingsFolder": "打开设置文件夹"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
+3
-2
@@ -1241,11 +1241,12 @@
|
|||||||
},
|
},
|
||||||
"noPaths": {
|
"noPaths": {
|
||||||
"title": "找不到其他模型資料夾",
|
"title": "找不到其他模型資料夾",
|
||||||
"descriptionStandalone": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將下方的資料夾路徑加入 settings.json,然後重新啟動 LoRA Manager。",
|
"descriptionStandalone": "其他模型管理已開啟,但找不到其他模型資料夾。請將您需要的資料夾鍵加入 settings.json 的 folder_paths 區段,然後重新啟動 LoRA Manager。",
|
||||||
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
|
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
|
||||||
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
|
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
|
||||||
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
|
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
|
||||||
"openSettings": "開啟設定"
|
"openSettings": "開啟設定",
|
||||||
|
"openSettingsFolder": "開啟設定資料夾"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
|
|||||||
@@ -421,6 +421,11 @@ def _wsl_to_windows_path(wsl_path: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_gui_display() -> bool:
|
||||||
|
"""Check whether a GUI session is reachable for xdg-open."""
|
||||||
|
return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
||||||
|
|
||||||
|
|
||||||
class PromptServerProtocol(Protocol):
|
class PromptServerProtocol(Protocol):
|
||||||
"""Subset of PromptServer used by the handlers."""
|
"""Subset of PromptServer used by the handlers."""
|
||||||
|
|
||||||
@@ -3393,6 +3398,18 @@ class FileSystemHandler:
|
|||||||
subprocess.Popen(["open", "-R", settings_file])
|
subprocess.Popen(["open", "-R", settings_file])
|
||||||
else:
|
else:
|
||||||
folder = os.path.dirname(settings_file)
|
folder = os.path.dirname(settings_file)
|
||||||
|
if not _has_gui_display():
|
||||||
|
# Headless/SSH session: xdg-open cannot open a file
|
||||||
|
# manager, so hand the path to the browser for copying
|
||||||
|
# instead of reporting a success that never happened.
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": "Headless session: path available for copying",
|
||||||
|
"path": settings_file,
|
||||||
|
"mode": "clipboard",
|
||||||
|
}
|
||||||
|
)
|
||||||
subprocess.Popen(["xdg-open", folder])
|
subprocess.Popen(["xdg-open", folder])
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|||||||
@@ -83,11 +83,16 @@ class OtherRoutes(BaseModelRoutes):
|
|||||||
# resolved to no existing folder. Render an actionable empty state
|
# resolved to no existing folder. Render an actionable empty state
|
||||||
# instead of an apparently broken empty grid.
|
# instead of an apparently broken empty grid.
|
||||||
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
||||||
return {
|
context = {
|
||||||
"other_disabled": False,
|
"other_disabled": False,
|
||||||
"other_no_paths": not bool(config.other_roots),
|
"other_no_paths": not bool(config.other_roots),
|
||||||
"standalone_mode": standalone_mode,
|
"standalone_mode": standalone_mode,
|
||||||
}
|
}
|
||||||
|
if standalone_mode:
|
||||||
|
# The settings UI cannot edit primary folder_paths, so the empty
|
||||||
|
# state must point at the actual file the user has to edit.
|
||||||
|
context["settings_file"] = getattr(self._settings, "settings_file", "") or ""
|
||||||
|
return context
|
||||||
|
|
||||||
def _get_expected_model_types(self) -> str:
|
def _get_expected_model_types(self) -> str:
|
||||||
"""Get expected model types string for error messages"""
|
"""Get expected model types string for error messages"""
|
||||||
|
|||||||
@@ -225,10 +225,9 @@ def main() -> int:
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Get project root (parent of .agents directory)
|
# Get project root: this script lives in <project_root>/scripts/e2e/.
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
skill_dir = os.path.dirname(script_dir)
|
project_root = os.path.dirname(os.path.dirname(script_dir))
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(skill_dir)))
|
|
||||||
|
|
||||||
managed_pids = read_managed_pids(args.port)
|
managed_pids = read_managed_pids(args.port)
|
||||||
|
|
||||||
|
|||||||
@@ -2432,6 +2432,11 @@ export class SettingsManager {
|
|||||||
|| ['vae', 'upscaler', 'text_encoder']
|
|| ['vae', 'upscaler', 'text_encoder']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const masterToggle = document.getElementById('enableOtherModels');
|
||||||
|
if (masterToggle) {
|
||||||
|
masterToggle.checked = enableOtherModels;
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('[data-other-subtype-toggle]').forEach((input) => {
|
document.querySelectorAll('[data-other-subtype-toggle]').forEach((input) => {
|
||||||
input.checked = enabledSubTypes.has(input.value);
|
input.checked = enabledSubTypes.has(input.value);
|
||||||
input.disabled = !enableOtherModels;
|
input.disabled = !enableOtherModels;
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.
|
|||||||
* empty state whose button turns the feature on; the backend then rebuilds the
|
* empty state whose button turns the feature on; the backend then rebuilds the
|
||||||
* other-model roots and starts scanning, so a reload lands on the real page.
|
* other-model roots and starts scanning, so a reload lands on the real page.
|
||||||
*
|
*
|
||||||
* The same module backs the "enabled but no folders found" state, where the
|
* The same module backs the "enabled but no folders found" state: ComfyUI
|
||||||
* only useful action is jumping to Settings instead of enabling anything.
|
* mode points to the Settings page's Library section, while standalone mode
|
||||||
|
* (where the settings UI cannot edit primary folder paths) reveals the
|
||||||
|
* settings.json file the user must edit instead.
|
||||||
*/
|
*/
|
||||||
async function handleEnableClick() {
|
async function handleEnableClick() {
|
||||||
const button = document.getElementById('enableOtherModelsBtn');
|
const button = document.getElementById('enableOtherModelsBtn');
|
||||||
@@ -32,6 +34,41 @@ function handleOpenSettingsClick(event) {
|
|||||||
openOtherModelsSettings();
|
openOtherModelsSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the settings.json location from the standalone no-folders state.
|
||||||
|
* The settings UI cannot edit primary folder_paths, so the only useful
|
||||||
|
* action is revealing the file itself (or copying its path in Docker).
|
||||||
|
*/
|
||||||
|
async function handleOpenSettingsFolderClick() {
|
||||||
|
const button = document.getElementById('openSettingsFolderBtn');
|
||||||
|
if (!button || button.disabled) return;
|
||||||
|
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lm/settings/open-location', { method: 'POST' });
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
if (data.mode === 'clipboard' && data.path) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(data.path);
|
||||||
|
showToast('settings.openSettingsFileLocation.copied', { path: data.path }, 'success');
|
||||||
|
} catch (clipboardError) {
|
||||||
|
console.warn('Clipboard API not available:', clipboardError);
|
||||||
|
showToast('settings.openSettingsFileLocation.clipboardFallback', { path: data.path }, 'info');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast('settings.openSettingsFileLocation.success', {}, 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open settings location:', error);
|
||||||
|
showToast('settings.openSettingsFileLocation.failed', {}, 'error');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function initializeOtherDisabledPage() {
|
async function initializeOtherDisabledPage() {
|
||||||
// appCore.initialize() wires the shared header (theme, settings modal,
|
// appCore.initialize() wires the shared header (theme, settings modal,
|
||||||
// language) so this page is not a dead end.
|
// language) so this page is not a dead end.
|
||||||
@@ -46,8 +83,13 @@ async function initializeOtherDisabledPage() {
|
|||||||
if (settingsButton) {
|
if (settingsButton) {
|
||||||
settingsButton.addEventListener('click', handleOpenSettingsClick);
|
settingsButton.addEventListener('click', handleOpenSettingsClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsFolderButton = document.getElementById('openSettingsFolderBtn');
|
||||||
|
if (settingsFolderButton) {
|
||||||
|
settingsFolderButton.addEventListener('click', handleOpenSettingsFolderClick);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
|
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
|
||||||
|
|
||||||
export { handleEnableClick as enableOtherModels, initializeOtherDisabledPage };
|
export { handleEnableClick as enableOtherModels, handleOpenSettingsFolderClick, initializeOtherDisabledPage };
|
||||||
|
|||||||
+20
-1
@@ -63,6 +63,19 @@
|
|||||||
background: rgba(127, 127, 127, 0.15);
|
background: rgba(127, 127, 127, 0.15);
|
||||||
border: 1px solid rgba(127, 127, 127, 0.25);
|
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||||
}
|
}
|
||||||
|
.other-settings-file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.other-settings-file code {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(127, 127, 127, 0.15);
|
||||||
|
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -127,6 +140,9 @@
|
|||||||
<h2>{{ t('other.noPaths.title') }}</h2>
|
<h2>{{ t('other.noPaths.title') }}</h2>
|
||||||
{% if standalone_mode %}
|
{% if standalone_mode %}
|
||||||
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
|
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
|
||||||
|
{% if settings_file %}
|
||||||
|
<p class="other-settings-file"><i class="fas fa-file-alt"></i> <code>{{ settings_file }}</code></p>
|
||||||
|
{% endif %}
|
||||||
<pre class="other-no-paths-config"><code>"folder_paths": {
|
<pre class="other-no-paths-config"><code>"folder_paths": {
|
||||||
"vae": ["/path/to/vae"],
|
"vae": ["/path/to/vae"],
|
||||||
"upscale_models": ["/path/to/upscale_models"],
|
"upscale_models": ["/path/to/upscale_models"],
|
||||||
@@ -135,13 +151,16 @@
|
|||||||
"controlnet": ["/path/to/controlnet"]
|
"controlnet": ["/path/to/controlnet"]
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
||||||
|
<button id="openSettingsFolderBtn" type="button">
|
||||||
|
<i class="fas fa-folder-open"></i> {{ t('other.noPaths.openSettingsFolder') }}
|
||||||
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ t('other.noPaths.descriptionComfyUI') }}</p>
|
<p>{{ t('other.noPaths.descriptionComfyUI') }}</p>
|
||||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintComfyUI') }}</p>
|
<p class="other-disabled-hint">{{ t('other.noPaths.hintComfyUI') }}</p>
|
||||||
{% endif %}
|
|
||||||
<button id="openOtherModelsSettingsBtn" type="button">
|
<button id="openOtherModelsSettingsBtn" type="button">
|
||||||
<i class="fas fa-cog"></i> {{ t('other.noPaths.openSettings') }}
|
<i class="fas fa-cog"></i> {{ t('other.noPaths.openSettings') }}
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="sticky-topbar">
|
<div class="sticky-topbar">
|
||||||
|
|||||||
@@ -665,6 +665,22 @@ describe('SettingsManager other-model root selects', () => {
|
|||||||
expect(container.classList.contains('is-disabled')).toBe(false);
|
expect(container.classList.contains('is-disabled')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('restores the master toggle checked state from settings', () => {
|
||||||
|
const manager = createManager();
|
||||||
|
const masterToggle = document.createElement('input');
|
||||||
|
masterToggle.type = 'checkbox';
|
||||||
|
masterToggle.id = 'enableOtherModels';
|
||||||
|
document.body.appendChild(masterToggle);
|
||||||
|
|
||||||
|
state.global.settings = { enable_other_models: true };
|
||||||
|
manager.updateOtherModelsControls();
|
||||||
|
expect(masterToggle.checked).toBe(true);
|
||||||
|
|
||||||
|
state.global.settings = { enable_other_models: false };
|
||||||
|
manager.updateOtherModelsControls();
|
||||||
|
expect(masterToggle.checked).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('persists the checked sub_types as the whole allow-list', async () => {
|
it('persists the checked sub_types as the whole allow-list', async () => {
|
||||||
const manager = createManager();
|
const manager = createManager();
|
||||||
appendToggles('vae', 'upscaler', 'controlnet');
|
appendToggles('vae', 'upscaler', 'controlnet');
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ describe('Other Models disabled page', () => {
|
|||||||
document.body.innerHTML = [
|
document.body.innerHTML = [
|
||||||
'<button id="enableOtherModelsBtn"></button>',
|
'<button id="enableOtherModelsBtn"></button>',
|
||||||
'<button id="openOtherModelsSettingsBtn"></button>',
|
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||||
|
'<button id="openSettingsFolderBtn"></button>',
|
||||||
].join('');
|
].join('');
|
||||||
|
|
||||||
Object.defineProperty(window, 'location', {
|
Object.defineProperty(window, 'location', {
|
||||||
@@ -64,6 +65,52 @@ describe('Other Models disabled page', () => {
|
|||||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reveals the settings.json location from the standalone no-folders state', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true, message: 'Opened settings folder' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = document.getElementById('openSettingsFolderBtn');
|
||||||
|
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'/api/lm/settings/open-location',
|
||||||
|
expect.objectContaining({ method: 'POST' }),
|
||||||
|
);
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith(
|
||||||
|
'settings.openSettingsFileLocation.success',
|
||||||
|
{},
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
expect(button.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies the settings path to the clipboard in Docker mode', async () => {
|
||||||
|
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||||
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
|
value: { writeText },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true, mode: 'clipboard', path: '/data/settings.json' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('openSettingsFolderBtn').dispatchEvent(
|
||||||
|
new MouseEvent('click', { bubbles: true }),
|
||||||
|
);
|
||||||
|
await vi.waitFor(() => expect(showToastMock).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(writeText).toHaveBeenCalledWith('/data/settings.json');
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith(
|
||||||
|
'settings.openSettingsFileLocation.copied',
|
||||||
|
{ path: '/data/settings.json' },
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('enables Other Models through the settings API and reloads', async () => {
|
it('enables Other Models through the settings API and reloads', async () => {
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -532,6 +532,62 @@ async def test_open_backup_location_uses_settings_directory(tmp_path, monkeypatc
|
|||||||
assert calls == [["xdg-open", str(backup_dir)]]
|
assert calls == [["xdg-open", str(backup_dir)]]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_open_settings_location_headless_returns_clipboard_mode(tmp_path, monkeypatch):
|
||||||
|
"""Without a GUI session xdg-open cannot work; the handler must hand the
|
||||||
|
path to the browser instead of reporting a success that never happened."""
|
||||||
|
settings_file = tmp_path / "settings" / "settings.json"
|
||||||
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings_file.write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
|
handler = FileSystemHandler(settings_service=SimpleNamespace(settings_file=str(settings_file)))
|
||||||
|
|
||||||
|
monkeypatch.delenv("DISPLAY", raising=False)
|
||||||
|
monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||||
|
|
||||||
|
popen_calls = []
|
||||||
|
monkeypatch.setattr(subprocess, "Popen", lambda *args, **kwargs: popen_calls.append(args))
|
||||||
|
|
||||||
|
response = await handler.open_settings_location(FakeRequest()) # pyright: ignore[reportArgumentType]
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert payload["mode"] == "clipboard"
|
||||||
|
assert payload["path"] == str(settings_file)
|
||||||
|
assert popen_calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_open_settings_location_with_display_opens_folder(tmp_path, monkeypatch):
|
||||||
|
settings_file = tmp_path / "settings" / "settings.json"
|
||||||
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings_file.write_text("{}", encoding="utf-8")
|
||||||
|
|
||||||
|
handler = FileSystemHandler(settings_service=SimpleNamespace(settings_file=str(settings_file)))
|
||||||
|
|
||||||
|
monkeypatch.setenv("DISPLAY", ":0")
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_docker", lambda: False)
|
||||||
|
monkeypatch.setattr("py.routes.handlers.misc_handlers._is_wsl", lambda: False)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_popen(args):
|
||||||
|
calls.append(args)
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
monkeypatch.setattr(subprocess, "Popen", fake_popen)
|
||||||
|
|
||||||
|
response = await handler.open_settings_location(FakeRequest()) # pyright: ignore[reportArgumentType]
|
||||||
|
payload = _json_payload(response)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert calls == [["xdg-open", str(settings_file.parent)]]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_open_wildcards_location_creates_and_opens_directory(tmp_path, monkeypatch):
|
async def test_open_wildcards_location_creates_and_opens_directory(tmp_path, monkeypatch):
|
||||||
wildcards_dir = tmp_path / "settings" / "wildcards"
|
wildcards_dir = tmp_path / "settings" / "wildcards"
|
||||||
|
|||||||
@@ -121,6 +121,25 @@ def test_page_context_reports_feature_state(monkeypatch):
|
|||||||
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
||||||
|
|
||||||
|
|
||||||
|
def test_page_context_exposes_settings_file_in_standalone(monkeypatch):
|
||||||
|
"""Standalone users must edit settings.json by hand; the empty state
|
||||||
|
needs the real file path to point them at."""
|
||||||
|
from py.config import config
|
||||||
|
from py.services.settings_manager import get_settings_manager
|
||||||
|
|
||||||
|
manager = get_settings_manager()
|
||||||
|
handler = OtherRoutes()
|
||||||
|
provider = handler._get_page_context_provider()
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "other_roots", [], raising=False)
|
||||||
|
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
||||||
|
|
||||||
|
context = provider(None)
|
||||||
|
assert context["other_no_paths"] is True
|
||||||
|
assert context["standalone_mode"] is True
|
||||||
|
assert context["settings_file"] == manager.settings_file
|
||||||
|
|
||||||
|
|
||||||
def test_get_expected_model_types_mentions_supported_types():
|
def test_get_expected_model_types_mentions_supported_types():
|
||||||
expected = OtherRoutes()._get_expected_model_types()
|
expected = OtherRoutes()._get_expected_model_types()
|
||||||
for name in ("VAE", "Upscaler", "TextEncoder", "CLIPVision", "Controlnet"):
|
for name in ("VAE", "Upscaler", "TextEncoder", "CLIPVision", "Controlnet"):
|
||||||
|
|||||||
Reference in New Issue
Block a user