mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(other-models): announce the feature only when folders are available
Other Models management is opt-in and its folders come from folder_paths.get_folder_paths(). In plugin mode ComfyUI registers vae, upscale_models, text_encoders, clip_vision and controlnet out of the box, so enabling the feature works immediately. Standalone only knows the keys present in settings.json.folder_paths, and that file is edited by hand - there is no UI for those keys - so a standalone user who followed the announcement banner reached "Enable Other Models" and then an empty page. Gate the announcement on the capability instead of on how the process was started: - Config.get_other_models_availability() probes every canonical other key (legacy clip collapses into text_encoders where the host exposes map_legacy) and reports which sub_types resolve to a folder that exists on disk. It deliberately ignores enable_other_models: the question is "could this work here at all?". An empty folder counts, because CivitAI downloads can target it. - /api/lm/settings exposes it as the derived, non-persisted other_models_paths_available flag; a probe failure yields null and the banner fails open. - BannerService only registers the announcement when the flag is not false. `=== false` (not falsy) keeps a cached/older payload working, and nothing is written to dismissed_banners, so the banner can return once folders exist. - The Other page grows an "enabled but nothing to scan" empty state driven by config.other_roots, showing the settings.json snippet for standalone and a pointer to ComfyUI model paths otherwise, plus an Open Settings action. It also covers the corner where only a non-default sub_type has a folder. Translate the six other.noPaths.* keys into all nine locales and record the new "folder key" / "on disk" terminology in the i18n guidelines. Backend tests and pytest tests/i18n could not run in this environment (no pytest/platformdirs); the probe was exercised against a stubbed folder_paths. Frontend: 120 files / 1101 JS tests passed.
This commit is contained in:
@@ -27,6 +27,12 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL).
|
||||
> default set — keep all three enumerating the full five (`VAE / upscaler / text encoder /
|
||||
> CLIP vision / ControlNet` in `en`; locale slash-list casing follows each file's existing
|
||||
> `VAE / Upscaler / Text Encoder / …` style, de compounds as `CLIP-Vision- und ControlNet-Ordner`).
|
||||
>
|
||||
> **Status (2026-09, "no folders found" state):** the Other Models page gained an *enabled but
|
||||
> nothing to scan* empty state with 6 new keys (`other.noPaths.*`); translated in all 9 locales
|
||||
> in the same pass. The `folder_paths` JSON snippet shown in that state lives in
|
||||
> `templates/other.html`, **not** in the locale files, so it is never translated — only the
|
||||
> surrounding prose is. Terminology added in §2.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,6 +279,19 @@ In prose these names sit next to localized nouns the same way `Diffusion Model`
|
||||
each locale's `header.filter.modelTypes` rendering (zh `管理的模型类型`, ja `管理するモデルタイプ`,
|
||||
de `Verwaltete Modelltypen`, …).
|
||||
|
||||
The "no folders found" empty state (`other.noPaths.*`) uses two phrases that must stay
|
||||
consistent whenever that copy is edited. `folder key` means the `folder_paths` key name
|
||||
(`vae`, `upscale_models`, … — Latin per the table above); `on disk` means the folder must
|
||||
physically exist:
|
||||
|
||||
| Phrase | Rendering |
|
||||
|---|---|
|
||||
| folder key | zh-CN 文件夹键 · zh-TW 資料夾鍵 · ja フォルダーキー · ko 폴더 키 · fr clé de dossier · de Ordnerschlüssel · es clave de carpeta · ru ключ папки · he מפתח תיקייה |
|
||||
| on disk | zh-CN 在磁盘上 · zh-TW 在磁碟上 · ja ディスク上 · ko 디스크에 · fr sur le disque · de auf dem Datenträger · es en el disco · ru на диске · he בדיסק |
|
||||
|
||||
`settings.json` and `ComfyUI` stay verbatim in every locale; "reload this page" / "restart
|
||||
LoRA Manager" reuse each locale's existing restart wording (`settings.extraFolderPaths.*`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Cross-cutting confusion hot-spots (must-fix list)
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "Aktivierung weiterer Modelle fehlgeschlagen",
|
||||
"downloadBlocked": "Die Verwaltung weiterer Modelle ist für diesen Modelltyp deaktiviert. Aktivieren Sie sie unter Einstellungen > Bibliothek, um diese Datei herunterzuladen.",
|
||||
"enableAction": "Weitere Modelle aktivieren"
|
||||
},
|
||||
"noPaths": {
|
||||
"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.",
|
||||
"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.",
|
||||
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
|
||||
"openSettings": "Einstellungen öffnen"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "Failed to enable Other Models",
|
||||
"downloadBlocked": "Other Models management is disabled for this model type. Enable it in Settings > Library to download this file.",
|
||||
"enableAction": "Enable Other Models"
|
||||
},
|
||||
"noPaths": {
|
||||
"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.",
|
||||
"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.",
|
||||
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
|
||||
"openSettings": "Open Settings"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "No se pudieron activar los otros modelos",
|
||||
"downloadBlocked": "La gestión de otros modelos está desactivada para este tipo de modelo. Actívala en Configuración > Biblioteca para descargar este archivo.",
|
||||
"enableAction": "Activar otros modelos"
|
||||
},
|
||||
"noPaths": {
|
||||
"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.",
|
||||
"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.",
|
||||
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
|
||||
"openSettings": "Abrir configuración"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "Échec de l’activation des autres modèles",
|
||||
"downloadBlocked": "La gestion des autres modèles est désactivée pour ce type de modèle. Activez-la dans Paramètres > Bibliothèque pour télécharger ce fichier.",
|
||||
"enableAction": "Activer les autres modèles"
|
||||
},
|
||||
"noPaths": {
|
||||
"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.",
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "הפעלת המודלים האחרים נכשלה",
|
||||
"downloadBlocked": "ניהול המודלים האחרים מושבת עבור סוג מודל זה. הפעל אותו בהגדרות > ספרייה כדי להוריד קובץ זה.",
|
||||
"enableAction": "הפעל מודלים אחרים"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "לא נמצאו תיקיות של מודלים אחרים",
|
||||
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את נתיבי התיקיות שלמטה ל-settings.json והפעל מחדש את LoRA Manager.",
|
||||
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
|
||||
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
|
||||
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
|
||||
"openSettings": "פתח הגדרות"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "その他のモデルの有効化に失敗しました",
|
||||
"downloadBlocked": "このモデルタイプではその他のモデル管理が無効です。このファイルをダウンロードするには「設定 > ライブラリ」で有効にしてください。",
|
||||
"enableAction": "その他のモデルを有効にする"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "その他のモデルのフォルダーが見つかりません",
|
||||
"descriptionStandalone": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。以下のフォルダーパスをsettings.jsonに追加し、LoRA Managerを再起動してください。",
|
||||
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
|
||||
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
|
||||
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
|
||||
"openSettings": "設定を開く"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "기타 모델 활성화 실패",
|
||||
"downloadBlocked": "이 모델 유형에 대해서는 기타 모델 관리가 비활성화되어 있습니다. 이 파일을 다운로드하려면 설정 > 라이브러리에서 활성화하세요.",
|
||||
"enableAction": "기타 모델 활성화"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "기타 모델 폴더를 찾을 수 없습니다",
|
||||
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 아래 폴더 경로를 settings.json에 추가한 뒤 LoRA Manager를 재시작하세요.",
|
||||
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
|
||||
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
|
||||
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
|
||||
"openSettings": "설정 열기"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "Не удалось включить другие модели",
|
||||
"downloadBlocked": "Управление другими моделями отключено для этого типа моделей. Включите его в разделе «Настройки > Библиотека», чтобы загрузить этот файл.",
|
||||
"enableAction": "Включить другие модели"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "Папки других моделей не найдены",
|
||||
"descriptionStandalone": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте указанные ниже пути к папкам в settings.json и перезапустите LoRA Manager.",
|
||||
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
|
||||
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
|
||||
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
|
||||
"openSettings": "Открыть настройки"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "启用其他模型失败",
|
||||
"downloadBlocked": "其他模型管理已对此模型类型禁用。请在“设置 > 库”中启用以下载此文件。",
|
||||
"enableAction": "启用其他模型"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "未找到其他模型文件夹",
|
||||
"descriptionStandalone": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将下面的文件夹路径添加到 settings.json,然后重启 LoRA Manager。",
|
||||
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
|
||||
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
|
||||
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
|
||||
"openSettings": "打开设置"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1232,6 +1232,14 @@
|
||||
"enableFailed": "啟用其他模型失敗",
|
||||
"downloadBlocked": "其他模型管理已對此模型類型停用。請在「設定 > 模型庫」中啟用以下載此檔案。",
|
||||
"enableAction": "啟用其他模型"
|
||||
},
|
||||
"noPaths": {
|
||||
"title": "找不到其他模型資料夾",
|
||||
"descriptionStandalone": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將下方的資料夾路徑加入 settings.json,然後重新啟動 LoRA Manager。",
|
||||
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
|
||||
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
|
||||
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
|
||||
"openSettings": "開啟設定"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
|
||||
@@ -1494,6 +1494,54 @@ class Config:
|
||||
self.other_roots = self._init_other_paths()
|
||||
self._rebuild_preview_roots()
|
||||
|
||||
def get_other_models_availability(self) -> Dict[str, Any]:
|
||||
"""Report the other-model folders the host can actually expose.
|
||||
|
||||
Independent of the opt-in ``enable_other_models`` toggle: this answers
|
||||
"could Other Models management work here at all?". ComfyUI mode almost
|
||||
always has these folder keys registered, while standalone mode only
|
||||
knows the keys present in ``settings.json.folder_paths`` - so the UI
|
||||
uses this to decide whether announcing the feature would be actionable.
|
||||
|
||||
Returns:
|
||||
``{"available": bool, "sub_types": {sub_type: [existing roots]}}``.
|
||||
A folder only counts when it exists on disk; an empty folder still
|
||||
counts because CivitAI downloads can target it.
|
||||
"""
|
||||
sub_types: Dict[str, List[str]] = {}
|
||||
try:
|
||||
keys = self._collapse_legacy_folder_keys(
|
||||
list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
|
||||
)
|
||||
except Exception: # pragma: no cover - defensive
|
||||
keys = list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
|
||||
|
||||
for key in keys:
|
||||
sub_type = OTHER_MODEL_FOLDER_SUBTYPES.get(key)
|
||||
if not sub_type:
|
||||
continue
|
||||
try:
|
||||
raw_paths = folder_paths.get_folder_paths(key)
|
||||
except Exception as exc:
|
||||
logger.debug("Error probing folder paths for '%s': %s", key, exc)
|
||||
continue
|
||||
|
||||
bucket = sub_types.setdefault(sub_type, [])
|
||||
for root in sorted(
|
||||
self._dedupe_existing_paths(raw_paths or []).values(),
|
||||
key=lambda path: path.lower(),
|
||||
):
|
||||
if root not in bucket:
|
||||
bucket.append(root)
|
||||
|
||||
available_sub_types = {
|
||||
sub_type: roots for sub_type, roots in sub_types.items() if roots
|
||||
}
|
||||
return {
|
||||
"available": bool(available_sub_types),
|
||||
"sub_types": available_sub_types,
|
||||
}
|
||||
|
||||
def get_preview_static_url(self, preview_path: str) -> str:
|
||||
if not preview_path:
|
||||
return ""
|
||||
|
||||
@@ -1559,6 +1559,22 @@ class SettingsHandler:
|
||||
response_data["civitai_api_key_set"] = bool(raw_key)
|
||||
raw_llm_key = self._settings.get("llm_api_key")
|
||||
response_data["llm_api_key_set"] = bool(raw_llm_key)
|
||||
# Derived capability flag (not persisted): whether the host exposes
|
||||
# any other-model folder at all. Standalone installs only know the
|
||||
# folder_paths keys present in settings.json, so the announcement
|
||||
# banner uses this to avoid promising a page that cannot list
|
||||
# anything.
|
||||
try:
|
||||
availability = config.get_other_models_availability()
|
||||
response_data["other_models_paths_available"] = bool(
|
||||
availability.get("available")
|
||||
)
|
||||
except Exception as availability_error: # pragma: no cover - defensive
|
||||
logger.debug(
|
||||
"Could not resolve Other Models availability: %s",
|
||||
availability_error,
|
||||
)
|
||||
response_data["other_models_paths_available"] = None
|
||||
settings_file = getattr(self._settings, "settings_file", None)
|
||||
if settings_file:
|
||||
response_data["settings_file"] = settings_file
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
from aiohttp import web
|
||||
|
||||
@@ -75,7 +76,18 @@ class OtherRoutes(BaseModelRoutes):
|
||||
return self._page_context_for_other
|
||||
|
||||
def _page_context_for_other(self, request: web.Request) -> Dict[str, Any]:
|
||||
return {"other_disabled": not self._settings.is_other_models_enabled()}
|
||||
if not self._settings.is_other_models_enabled():
|
||||
return {"other_disabled": True, "other_no_paths": False}
|
||||
|
||||
# Enabled but nothing to scan: folder paths for the managed sub_types
|
||||
# resolved to no existing folder. Render an actionable empty state
|
||||
# instead of an apparently broken empty grid.
|
||||
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
|
||||
return {
|
||||
"other_disabled": False,
|
||||
"other_no_paths": not bool(config.other_roots),
|
||||
"standalone_mode": standalone_mode,
|
||||
}
|
||||
|
||||
def _get_expected_model_types(self) -> str:
|
||||
"""Get expected model types string for error messages"""
|
||||
|
||||
@@ -553,6 +553,13 @@ class BannerService {
|
||||
if (state.global.settings.enable_other_models) {
|
||||
return;
|
||||
}
|
||||
// Only announce when the host can actually resolve other-model folders.
|
||||
// Standalone installs only know the folder_paths keys present in
|
||||
// settings.json, so announcing there would land the user on an empty
|
||||
// page. `=== false` (not falsy) keeps older payloads working.
|
||||
if (state.global.settings.other_models_paths_available === false) {
|
||||
return;
|
||||
}
|
||||
if (this.isBannerDismissed(OTHER_MODELS_BANNER_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { appCore } from './core.js';
|
||||
import { showToast } from './utils/uiHelpers.js';
|
||||
import { enableOtherModels } from './utils/otherModels.js';
|
||||
import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.js';
|
||||
|
||||
/**
|
||||
* Other Models is an opt-in feature. While it is disabled this page renders an
|
||||
* 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.
|
||||
*
|
||||
* The same module backs the "enabled but no folders found" state, where the
|
||||
* only useful action is jumping to Settings instead of enabling anything.
|
||||
*/
|
||||
async function handleEnableClick() {
|
||||
const button = document.getElementById('enableOtherModelsBtn');
|
||||
@@ -20,6 +23,15 @@ async function handleEnableClick() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Settings on the Library section for the "no folders found" state, so a
|
||||
* misconfigured install can be fixed without hand-editing unknown keys.
|
||||
*/
|
||||
function handleOpenSettingsClick(event) {
|
||||
event.preventDefault();
|
||||
openOtherModelsSettings();
|
||||
}
|
||||
|
||||
async function initializeOtherDisabledPage() {
|
||||
// appCore.initialize() wires the shared header (theme, settings modal,
|
||||
// language) so this page is not a dead end.
|
||||
@@ -29,6 +41,11 @@ async function initializeOtherDisabledPage() {
|
||||
if (button) {
|
||||
button.addEventListener('click', handleEnableClick);
|
||||
}
|
||||
|
||||
const settingsButton = document.getElementById('openOtherModelsSettingsBtn');
|
||||
if (settingsButton) {
|
||||
settingsButton.addEventListener('click', handleOpenSettingsClick);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
|
||||
|
||||
+35
-1
@@ -51,6 +51,18 @@
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.other-no-paths-config {
|
||||
margin: 4px 0 0;
|
||||
padding: 12px 16px;
|
||||
max-width: 520px;
|
||||
overflow-x: auto;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 6px;
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -109,6 +121,28 @@
|
||||
</button>
|
||||
<p class="other-disabled-hint">{{ t('other.disabled.hint') }}</p>
|
||||
</div>
|
||||
{% elif other_no_paths %}
|
||||
<div class="other-disabled">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<h2>{{ t('other.noPaths.title') }}</h2>
|
||||
{% if standalone_mode %}
|
||||
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
|
||||
<pre class="other-no-paths-config"><code>"folder_paths": {
|
||||
"vae": ["/path/to/vae"],
|
||||
"upscale_models": ["/path/to/upscale_models"],
|
||||
"text_encoders": ["/path/to/text_encoders"],
|
||||
"clip_vision": ["/path/to/clip_vision"],
|
||||
"controlnet": ["/path/to/controlnet"]
|
||||
}</code></pre>
|
||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
|
||||
{% else %}
|
||||
<p>{{ t('other.noPaths.descriptionComfyUI') }}</p>
|
||||
<p class="other-disabled-hint">{{ t('other.noPaths.hintComfyUI') }}</p>
|
||||
{% endif %}
|
||||
<button id="openOtherModelsSettingsBtn" type="button">
|
||||
<i class="fas fa-cog"></i> {{ t('other.noPaths.openSettings') }}
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="sticky-topbar">
|
||||
{% include 'components/controls.html' %}
|
||||
@@ -129,7 +163,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
{% if other_disabled %}
|
||||
{% if other_disabled or other_no_paths %}
|
||||
<script type="module" src="/loras_static/js/other_disabled.js?v={{ version }}"></script>
|
||||
{% else %}
|
||||
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
|
||||
|
||||
@@ -414,3 +414,74 @@ class TestOtherRootsWiring:
|
||||
config._rebuild_preview_roots()
|
||||
|
||||
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
|
||||
|
||||
|
||||
class TestOtherModelsAvailability:
|
||||
"""Config.get_other_models_availability ignores the opt-in toggle.
|
||||
|
||||
It answers "could Other Models work here at all?", which the settings
|
||||
payload and the announcement banner use to avoid promising a page that
|
||||
cannot list anything.
|
||||
"""
|
||||
|
||||
def _stub_folder_paths(self, monkeypatch, mapping):
|
||||
def get_folder_paths(key):
|
||||
value = mapping.get(key, [])
|
||||
return [value] if isinstance(value, str) else list(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_module.folder_paths, "get_folder_paths", get_folder_paths
|
||||
)
|
||||
# No host alias rewriting: every key stays independently queryable,
|
||||
# which is what the standalone mock does.
|
||||
monkeypatch.delattr(config_module.folder_paths, "map_legacy", raising=False)
|
||||
|
||||
def test_reports_available_when_a_folder_exists(self, monkeypatch, tmp_path):
|
||||
vae_dir = tmp_path / "vae"
|
||||
vae_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||
|
||||
# The feature stays off on purpose: availability must not depend on it.
|
||||
get_settings_manager().set("enable_other_models", False)
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
assert availability["sub_types"] == {"vae": [_normalize(str(vae_dir))]}
|
||||
|
||||
def test_counts_an_empty_but_existing_folder(self, monkeypatch, tmp_path):
|
||||
vae_dir = tmp_path / "vae"
|
||||
vae_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
|
||||
def test_ignores_missing_folders(self, monkeypatch, tmp_path):
|
||||
self._stub_folder_paths(
|
||||
monkeypatch, {"vae": str(tmp_path / "does-not-exist")}
|
||||
)
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability == {"available": False, "sub_types": {}}
|
||||
|
||||
def test_reports_unavailable_without_any_configuration(self, monkeypatch):
|
||||
self._stub_folder_paths(monkeypatch, {})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability == {"available": False, "sub_types": {}}
|
||||
|
||||
def test_merges_legacy_clip_key_into_text_encoder(self, monkeypatch, tmp_path):
|
||||
clip_dir = tmp_path / "clip"
|
||||
clip_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"clip": [str(clip_dir)]})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
assert availability["sub_types"] == {
|
||||
"text_encoder": [_normalize(str(clip_dir))]
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ describe('BannerService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
state.global.settings.enable_other_models = false;
|
||||
state.global.settings.other_models_paths_available = true;
|
||||
});
|
||||
|
||||
it('announces the feature while it is switched off', () => {
|
||||
@@ -225,6 +226,25 @@ describe('BannerService', () => {
|
||||
.toContain('Other Models Management is available');
|
||||
});
|
||||
|
||||
it('stays silent when the host exposes no other-model folders', () => {
|
||||
// Standalone installs without the folder_paths keys in
|
||||
// settings.json would land on an empty page, so do not announce.
|
||||
state.global.settings.other_models_paths_available = false;
|
||||
|
||||
prepareBanner();
|
||||
|
||||
expect(bannerElement()).toBeNull();
|
||||
expect(bannerService.banners.has(OTHER_MODELS_BANNER_ID)).toBe(false);
|
||||
});
|
||||
|
||||
it('still announces when availability is unknown (older payload)', () => {
|
||||
delete state.global.settings.other_models_paths_available;
|
||||
|
||||
prepareBanner();
|
||||
|
||||
expect(bannerElement()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('stays silent once the feature is enabled', () => {
|
||||
state.global.settings.enable_other_models = true;
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ describe('Other Models disabled page', () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
initializeAppMock.mockResolvedValue(undefined);
|
||||
document.body.innerHTML = '<button id="enableOtherModelsBtn"></button>';
|
||||
document.body.innerHTML = [
|
||||
'<button id="enableOtherModelsBtn"></button>',
|
||||
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||
].join('');
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, reload: vi.fn() },
|
||||
@@ -43,12 +46,24 @@ describe('Other Models disabled page', () => {
|
||||
writable: true,
|
||||
});
|
||||
delete global.fetch;
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('boots the shared app core so the header stays usable', () => {
|
||||
expect(initializeAppMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens the Library settings from the no-folders state', () => {
|
||||
const showModal = vi.fn();
|
||||
window.modalManager = { showModal };
|
||||
|
||||
document.getElementById('openOtherModelsSettingsBtn').dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true }),
|
||||
);
|
||||
|
||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||
});
|
||||
|
||||
it('enables Other Models through the settings API and reloads', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
'civitai_api_key_set': True,
|
||||
'language': 'en',
|
||||
'llm_api_key_set': False,
|
||||
'other_models_paths_available': False,
|
||||
'theme': 'dark',
|
||||
}),
|
||||
'success': True,
|
||||
|
||||
@@ -98,17 +98,27 @@ def test_validate_rejects_switched_off_sub_type():
|
||||
assert handler._validate_civitai_model_type("Upscaler") is False
|
||||
|
||||
|
||||
def test_page_context_reports_feature_state():
|
||||
def test_page_context_reports_feature_state(monkeypatch):
|
||||
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()
|
||||
|
||||
assert provider(None) == {"other_disabled": False}
|
||||
monkeypatch.setattr(config, "other_roots", ["/models/vae"], raising=False)
|
||||
context = provider(None)
|
||||
assert context["other_disabled"] is False
|
||||
assert context["other_no_paths"] is False
|
||||
|
||||
# Enabled but nothing resolved: the page must explain how to fix it.
|
||||
monkeypatch.setattr(config, "other_roots", [], raising=False)
|
||||
context = provider(None)
|
||||
assert context["other_disabled"] is False
|
||||
assert context["other_no_paths"] is True
|
||||
|
||||
manager.set("enable_other_models", False)
|
||||
assert provider(None) == {"other_disabled": True}
|
||||
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
||||
|
||||
|
||||
def test_get_expected_model_types_mentions_supported_types():
|
||||
|
||||
Reference in New Issue
Block a user