diff --git a/docs/i18n-translation-guidelines.md b/docs/i18n-translation-guidelines.md index 957473be..0d086e43 100644 --- a/docs/i18n-translation-guidelines.md +++ b/docs/i18n-translation-guidelines.md @@ -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) diff --git a/locales/de.json b/locales/de.json index eeed8566..881408bb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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": { diff --git a/locales/en.json b/locales/en.json index 3dc54da7..849e19fc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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": { diff --git a/locales/es.json b/locales/es.json index 7e908542..00a5412a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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": { diff --git a/locales/fr.json b/locales/fr.json index c76fb77f..2d022e0e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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": { diff --git a/locales/he.json b/locales/he.json index 3f77d2dd..f0d1c31e 100644 --- a/locales/he.json +++ b/locales/he.json @@ -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": { diff --git a/locales/ja.json b/locales/ja.json index 39b072bb..ba03571e 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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": { diff --git a/locales/ko.json b/locales/ko.json index d835f614..6e7e869d 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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": { diff --git a/locales/ru.json b/locales/ru.json index 5ba42a7e..72a78535 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index ee3400dc..fd2c6931 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 5681ffc6..ea481ef2 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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": { diff --git a/py/config.py b/py/config.py index 56dadef8..3c5465c4 100644 --- a/py/config.py +++ b/py/config.py @@ -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 "" diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index e88c198f..a1519756 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -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 diff --git a/py/routes/other_routes.py b/py/routes/other_routes.py index 8615d97b..eedf1063 100644 --- a/py/routes/other_routes.py +++ b/py/routes/other_routes.py @@ -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""" diff --git a/static/js/managers/BannerService.js b/static/js/managers/BannerService.js index 37f187a1..746ccdce 100644 --- a/static/js/managers/BannerService.js +++ b/static/js/managers/BannerService.js @@ -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; } diff --git a/static/js/other_disabled.js b/static/js/other_disabled.js index 7ae5462a..950a4482 100644 --- a/static/js/other_disabled.js +++ b/static/js/other_disabled.js @@ -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); diff --git a/templates/other.html b/templates/other.html index fc16aa9a..492d73b6 100644 --- a/templates/other.html +++ b/templates/other.html @@ -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); + } {% endblock %} @@ -109,6 +121,28 @@

{{ t('other.disabled.hint') }}

+ {% elif other_no_paths %} +
+ +

{{ t('other.noPaths.title') }}

+ {% if standalone_mode %} +

{{ t('other.noPaths.descriptionStandalone') }}

+
"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"]
+}
+

{{ t('other.noPaths.hintStandalone') }}

+ {% else %} +

{{ t('other.noPaths.descriptionComfyUI') }}

+

{{ t('other.noPaths.hintComfyUI') }}

+ {% endif %} + +
{% else %}
{% include 'components/controls.html' %} @@ -129,7 +163,7 @@ {% endblock %} {% block main_script %} -{% if other_disabled %} +{% if other_disabled or other_no_paths %} {% else %} diff --git a/tests/config/test_other_paths.py b/tests/config/test_other_paths.py index c993f42f..67ad23c9 100644 --- a/tests/config/test_other_paths.py +++ b/tests/config/test_other_paths.py @@ -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))] + } diff --git a/tests/frontend/managers/BannerService.test.js b/tests/frontend/managers/BannerService.test.js index e87036f6..527da151 100644 --- a/tests/frontend/managers/BannerService.test.js +++ b/tests/frontend/managers/BannerService.test.js @@ -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; diff --git a/tests/frontend/pages/otherDisabledPage.test.js b/tests/frontend/pages/otherDisabledPage.test.js index 7ae2ea31..4d240ffd 100644 --- a/tests/frontend/pages/otherDisabledPage.test.js +++ b/tests/frontend/pages/otherDisabledPage.test.js @@ -22,7 +22,10 @@ describe('Other Models disabled page', () => { vi.resetModules(); vi.clearAllMocks(); initializeAppMock.mockResolvedValue(undefined); - document.body.innerHTML = ''; + document.body.innerHTML = [ + '', + '', + ].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, diff --git a/tests/routes/__snapshots__/test_api_snapshots.ambr b/tests/routes/__snapshots__/test_api_snapshots.ambr index c2ee524b..8cd53f95 100644 --- a/tests/routes/__snapshots__/test_api_snapshots.ambr +++ b/tests/routes/__snapshots__/test_api_snapshots.ambr @@ -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, diff --git a/tests/routes/test_other_routes.py b/tests/routes/test_other_routes.py index bfa2cd86..3defdc35 100644 --- a/tests/routes/test_other_routes.py +++ b/tests/routes/test_other_routes.py @@ -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():