mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
fix(settings): sync portable mode toggle
This commit is contained in:
@@ -232,8 +232,13 @@
|
||||
"exampleImages": "Beispielbilder",
|
||||
"misc": "Verschiedenes",
|
||||
"metadataArchive": "Metadaten-Archiv-Datenbank",
|
||||
"storageLocation": "Einstellungsort",
|
||||
"proxySettings": "Proxy-Einstellungen"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Portabler Modus",
|
||||
"locationHelp": "Aktiviere, um settings.json im Repository zu belassen; deaktiviere, um es im Benutzerkonfigurationsordner zu speichern."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "NSFW-Inhalte unscharf stellen",
|
||||
"blurNsfwContentHelp": "Nicht jugendfreie (NSFW) Vorschaubilder unscharf stellen",
|
||||
|
||||
@@ -232,8 +232,13 @@
|
||||
"exampleImages": "Example Images",
|
||||
"misc": "Misc.",
|
||||
"metadataArchive": "Metadata Archive Database",
|
||||
"storageLocation": "Settings Location",
|
||||
"proxySettings": "Proxy Settings"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Portable mode",
|
||||
"locationHelp": "Enable to keep settings.json inside the repository; disable to store it in your user config directory."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Blur NSFW Content",
|
||||
"blurNsfwContentHelp": "Blur mature (NSFW) content preview images",
|
||||
|
||||
@@ -232,8 +232,13 @@
|
||||
"exampleImages": "Imágenes de ejemplo",
|
||||
"misc": "Varios",
|
||||
"metadataArchive": "Base de datos de archivo de metadatos",
|
||||
"storageLocation": "Ubicación de ajustes",
|
||||
"proxySettings": "Configuración de proxy"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Modo portátil",
|
||||
"locationHelp": "Activa para mantener settings.json dentro del repositorio; desactívalo para guardarlo en tu directorio de configuración de usuario."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Difuminar contenido NSFW",
|
||||
"blurNsfwContentHelp": "Difuminar imágenes de vista previa de contenido para adultos (NSFW)",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "Images d'exemple",
|
||||
"misc": "Divers",
|
||||
"metadataArchive": "Base de données d'archive des métadonnées",
|
||||
"storageLocation": "Emplacement des paramètres",
|
||||
"proxySettings": "Paramètres du proxy",
|
||||
"priorityTags": "Étiquettes prioritaires"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Mode portable",
|
||||
"locationHelp": "Activez pour garder settings.json dans le dépôt ; désactivez pour le placer dans votre dossier de configuration utilisateur."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Flouter le contenu NSFW",
|
||||
"blurNsfwContentHelp": "Flouter les images d'aperçu de contenu pour adultes (NSFW)",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "תמונות דוגמה",
|
||||
"misc": "שונות",
|
||||
"metadataArchive": "מסד נתונים של ארכיון מטא-דאטה",
|
||||
"storageLocation": "מיקום ההגדרות",
|
||||
"proxySettings": "הגדרות פרוקסי",
|
||||
"priorityTags": "תגיות עדיפות"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "מצב נייד",
|
||||
"locationHelp": "הפעל כדי לשמור את settings.json בתוך המאגר; בטל כדי לשמור אותו בתיקיית ההגדרות של המשתמש."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "טשטש תוכן NSFW",
|
||||
"blurNsfwContentHelp": "טשטש תמונות תצוגה מקדימה של תוכן למבוגרים (NSFW)",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "例画像",
|
||||
"misc": "その他",
|
||||
"metadataArchive": "メタデータアーカイブデータベース",
|
||||
"storageLocation": "設定の場所",
|
||||
"proxySettings": "プロキシ設定",
|
||||
"priorityTags": "優先タグ"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "ポータブルモード",
|
||||
"locationHelp": "有効にすると settings.json をリポジトリ内に保持し、無効にするとユーザー設定ディレクトリに格納します。"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "NSFWコンテンツをぼかす",
|
||||
"blurNsfwContentHelp": "成人向け(NSFW)コンテンツのプレビュー画像をぼかします",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "예시 이미지",
|
||||
"misc": "기타",
|
||||
"metadataArchive": "메타데이터 아카이브 데이터베이스",
|
||||
"storageLocation": "설정 위치",
|
||||
"proxySettings": "프록시 설정",
|
||||
"priorityTags": "우선순위 태그"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "휴대용 모드",
|
||||
"locationHelp": "활성화하면 settings.json을 리포지토리에 유지하고, 비활성화하면 사용자 구성 디렉터리에 저장합니다."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "NSFW 콘텐츠 블러 처리",
|
||||
"blurNsfwContentHelp": "성인(NSFW) 콘텐츠 미리보기 이미지를 블러 처리합니다",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "Примеры изображений",
|
||||
"misc": "Разное",
|
||||
"metadataArchive": "Архив метаданных",
|
||||
"storageLocation": "Расположение настроек",
|
||||
"proxySettings": "Настройки прокси",
|
||||
"priorityTags": "Приоритетные теги"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Портативный режим",
|
||||
"locationHelp": "Включите, чтобы хранить settings.json в репозитории; выключите, чтобы сохранить его в папке конфигурации пользователя."
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Размывать NSFW контент",
|
||||
"blurNsfwContentHelp": "Размывать превью изображений контента для взрослых (NSFW)",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "示例图片",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "元数据归档数据库",
|
||||
"storageLocation": "设置位置",
|
||||
"proxySettings": "代理设置",
|
||||
"priorityTags": "优先标签"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "便携模式",
|
||||
"locationHelp": "开启可将 settings.json 保存在仓库中;关闭则保存在用户配置目录。"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "模糊 NSFW 内容",
|
||||
"blurNsfwContentHelp": "模糊成熟(NSFW)内容预览图片",
|
||||
|
||||
@@ -231,9 +231,14 @@
|
||||
"exampleImages": "範例圖片",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "中繼資料封存資料庫",
|
||||
"storageLocation": "設定位置",
|
||||
"proxySettings": "代理設定",
|
||||
"priorityTags": "優先標籤"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "可攜式模式",
|
||||
"locationHelp": "啟用可將 settings.json 保存在儲存庫中;停用則保存在使用者設定目錄。"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "模糊 NSFW 內容",
|
||||
"blurNsfwContentHelp": "模糊成熟(NSFW)內容預覽圖片",
|
||||
|
||||
@@ -180,6 +180,7 @@ class SettingsHandler:
|
||||
"download_path_templates",
|
||||
"enable_metadata_archive_db",
|
||||
"language",
|
||||
"use_portable_settings",
|
||||
"proxy_enabled",
|
||||
"proxy_type",
|
||||
"proxy_host",
|
||||
|
||||
@@ -2,14 +2,17 @@ import asyncio
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from threading import Lock
|
||||
from typing import Any, Awaitable, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
from platformdirs import user_config_dir
|
||||
|
||||
from ..utils.constants import DEFAULT_HASH_CHUNK_SIZE_MB, DEFAULT_PRIORITY_TAG_CONFIG
|
||||
from ..utils.settings_paths import ensure_settings_file
|
||||
from ..utils.settings_paths import APP_NAME, ensure_settings_file, get_legacy_settings_path
|
||||
from ..utils.tag_priorities import (
|
||||
PriorityTagEntry,
|
||||
collect_canonical_tags,
|
||||
@@ -64,6 +67,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
class SettingsManager:
|
||||
def __init__(self):
|
||||
self.settings_file = ensure_settings_file(logger)
|
||||
self._pending_portable_switch: Optional[Dict[str, str]] = None
|
||||
self._standalone_mode = self._detect_standalone_mode()
|
||||
self._startup_messages: List[Dict[str, Any]] = []
|
||||
self._needs_initial_save = False
|
||||
@@ -783,6 +787,10 @@ class SettingsManager:
|
||||
def set(self, key: str, value: Any) -> None:
|
||||
"""Set setting value and save"""
|
||||
self.settings[key] = value
|
||||
portable_switch_pending = False
|
||||
if key == "use_portable_settings" and isinstance(value, bool):
|
||||
portable_switch_pending = True
|
||||
self._prepare_portable_switch(value)
|
||||
if key == 'folder_paths' and isinstance(value, Mapping):
|
||||
self._update_active_library_entry(folder_paths=value) # type: ignore[arg-type]
|
||||
elif key == 'default_lora_root':
|
||||
@@ -794,6 +802,8 @@ class SettingsManager:
|
||||
elif key == 'model_name_display':
|
||||
self._notify_model_name_display_change(value)
|
||||
self._save_settings()
|
||||
if portable_switch_pending:
|
||||
self._finalize_portable_switch()
|
||||
|
||||
def delete(self, key: str) -> None:
|
||||
"""Delete setting key and save"""
|
||||
@@ -802,6 +812,113 @@ class SettingsManager:
|
||||
self._save_settings()
|
||||
logger.info(f"Deleted setting: {key}")
|
||||
|
||||
def _prepare_portable_switch(self, use_portable: bool) -> None:
|
||||
"""Prepare switching the settings storage location."""
|
||||
|
||||
legacy_path = get_legacy_settings_path()
|
||||
user_dir = self._get_user_config_directory()
|
||||
user_settings_path = os.path.join(user_dir, "settings.json")
|
||||
|
||||
target_path = legacy_path if use_portable else user_settings_path
|
||||
other_path = user_settings_path if use_portable else legacy_path
|
||||
target_dir = os.path.dirname(target_path)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
|
||||
previous_path = self.settings_file or target_path
|
||||
previous_dir = os.path.dirname(previous_path) or target_dir
|
||||
|
||||
if os.path.abspath(previous_path) != os.path.abspath(target_path):
|
||||
self._copy_model_cache_directory(previous_dir, target_dir)
|
||||
|
||||
self._pending_portable_switch = {"other_path": other_path}
|
||||
self.settings_file = target_path
|
||||
|
||||
def _finalize_portable_switch(self) -> None:
|
||||
"""Mirror the latest settings file to the secondary location."""
|
||||
|
||||
info = self._pending_portable_switch
|
||||
if not info:
|
||||
return
|
||||
|
||||
other_path = info.get("other_path")
|
||||
current_path = self.settings_file
|
||||
|
||||
if not other_path or not current_path:
|
||||
self._pending_portable_switch = None
|
||||
return
|
||||
|
||||
if os.path.abspath(other_path) == os.path.abspath(current_path):
|
||||
self._pending_portable_switch = None
|
||||
return
|
||||
|
||||
other_dir = os.path.dirname(other_path) or os.path.dirname(current_path)
|
||||
if other_dir:
|
||||
os.makedirs(other_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
shutil.copy2(current_path, other_path)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to mirror settings.json to %s: %s", other_path, exc)
|
||||
finally:
|
||||
self._pending_portable_switch = None
|
||||
|
||||
def _copy_model_cache_directory(self, source_dir: str, target_dir: str) -> None:
|
||||
"""Copy model_cache artifacts when switching storage locations."""
|
||||
|
||||
if not source_dir or not target_dir:
|
||||
return
|
||||
|
||||
source_cache_dir = os.path.join(source_dir, "model_cache")
|
||||
target_cache_dir = os.path.join(target_dir, "model_cache")
|
||||
if (
|
||||
os.path.isdir(source_cache_dir)
|
||||
and os.path.abspath(source_cache_dir) != os.path.abspath(target_cache_dir)
|
||||
):
|
||||
try:
|
||||
shutil.copytree(source_cache_dir, target_cache_dir, dirs_exist_ok=True)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to copy model_cache directory from %s to %s: %s",
|
||||
source_cache_dir,
|
||||
target_cache_dir,
|
||||
exc,
|
||||
)
|
||||
|
||||
source_cache_file = os.path.join(source_dir, "model_cache.sqlite")
|
||||
target_cache_file = os.path.join(target_dir, "model_cache.sqlite")
|
||||
if (
|
||||
os.path.isfile(source_cache_file)
|
||||
and os.path.abspath(source_cache_file) != os.path.abspath(target_cache_file)
|
||||
):
|
||||
try:
|
||||
shutil.copy2(source_cache_file, target_cache_file)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to copy model_cache.sqlite from %s to %s: %s",
|
||||
source_cache_file,
|
||||
target_cache_file,
|
||||
exc,
|
||||
)
|
||||
|
||||
def _get_user_config_directory(self) -> str:
|
||||
"""Return the user configuration directory, falling back to ~/.config."""
|
||||
|
||||
try:
|
||||
config_dir = user_config_dir(APP_NAME, appauthor=False) or ""
|
||||
except Exception as exc: # pragma: no cover - defensive fallback
|
||||
logger.warning("Failed to determine user config directory: %s", exc)
|
||||
config_dir = ""
|
||||
|
||||
if not config_dir:
|
||||
config_dir = os.path.join(os.path.expanduser("~"), f".config/{APP_NAME}")
|
||||
|
||||
try:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to create user config directory %s: %s", config_dir, exc)
|
||||
|
||||
return config_dir
|
||||
|
||||
def _notify_model_name_display_change(self, value: Any) -> None:
|
||||
"""Trigger cache resorting when the model name display preference updates."""
|
||||
|
||||
|
||||
@@ -371,6 +371,11 @@ export class SettingsManager {
|
||||
showOnlySFWCheckbox.checked = state.global.settings.show_only_sfw ?? false;
|
||||
}
|
||||
|
||||
const usePortableCheckbox = document.getElementById('usePortableSettings');
|
||||
if (usePortableCheckbox) {
|
||||
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
|
||||
}
|
||||
|
||||
// Set video autoplay on hover setting
|
||||
const autoplayOnHoverCheckbox = document.getElementById('autoplayOnHover');
|
||||
if (autoplayOnHoverCheckbox) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/co
|
||||
|
||||
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
civitai_api_key: '',
|
||||
use_portable_settings: false,
|
||||
language: 'en',
|
||||
show_only_sfw: false,
|
||||
enable_metadata_archive_db: false,
|
||||
|
||||
@@ -38,6 +38,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.sections.storageLocation') }}</h3>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="usePortableSettings">{{ t('settings.storage.locationLabel') }}</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="usePortableSettings" {% if settings.get('use_portable_settings', False) %}checked{% endif %}
|
||||
onchange="settingsManager.saveToggleSetting('usePortableSettings', 'use_portable_settings')">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
{{ t('settings.storage.locationHelp') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.sections.contentFiltering') }}</h3>
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import threading
|
||||
import json
|
||||
import os
|
||||
from concurrent.futures import Future
|
||||
|
||||
import pytest
|
||||
|
||||
from py.services import service_registry
|
||||
from py.services import settings_manager as settings_manager_module
|
||||
from py.services.settings_manager import SettingsManager
|
||||
from py.utils import settings_paths
|
||||
|
||||
@@ -163,6 +164,68 @@ def _create_manager_with_settings(tmp_path, monkeypatch, initial_settings, *, sa
|
||||
return mgr
|
||||
|
||||
|
||||
def _setup_storage_paths(tmp_path, monkeypatch):
|
||||
project_root = tmp_path / "repo"
|
||||
project_root.mkdir(parents=True, exist_ok=True)
|
||||
user_dir = tmp_path / "user_config"
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
user_settings_path = user_dir / "settings.json"
|
||||
|
||||
monkeypatch.setattr(
|
||||
"py.services.settings_manager.ensure_settings_file",
|
||||
lambda logger=None: str(user_settings_path),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
settings_manager_module,
|
||||
"user_config_dir",
|
||||
lambda *args, **kwargs: str(user_dir),
|
||||
)
|
||||
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(project_root))
|
||||
return project_root, user_dir, user_settings_path
|
||||
|
||||
|
||||
def _populate_cache(root_dir, marker_name, db_text):
|
||||
cache_dir = root_dir / "model_cache"
|
||||
cache_dir.mkdir(exist_ok=True)
|
||||
marker_file = cache_dir / marker_name
|
||||
marker_file.write_text(marker_name, encoding="utf-8")
|
||||
(root_dir / "model_cache.sqlite").write_text(db_text, encoding="utf-8")
|
||||
|
||||
|
||||
def test_switch_to_portable_mode_copies_cache(tmp_path, monkeypatch):
|
||||
project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch)
|
||||
_populate_cache(user_dir, "user_marker.txt", "user_db")
|
||||
|
||||
manager = SettingsManager()
|
||||
|
||||
manager.set("use_portable_settings", True)
|
||||
|
||||
assert manager.settings_file == str(project_root / "settings.json")
|
||||
marker_copy = project_root / "model_cache" / "user_marker.txt"
|
||||
assert marker_copy.read_text(encoding="utf-8") == "user_marker.txt"
|
||||
assert (project_root / "model_cache.sqlite").read_text(encoding="utf-8") == "user_db"
|
||||
assert user_settings.exists()
|
||||
|
||||
|
||||
def test_switching_back_to_user_config_moves_cache(tmp_path, monkeypatch):
|
||||
project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch)
|
||||
_populate_cache(user_dir, "user_marker.txt", "user_db")
|
||||
|
||||
manager = SettingsManager()
|
||||
manager.set("use_portable_settings", True)
|
||||
|
||||
project_cache_dir = project_root / "model_cache"
|
||||
project_cache_dir.mkdir(exist_ok=True)
|
||||
(project_cache_dir / "project_marker.txt").write_text("project_marker", encoding="utf-8")
|
||||
(project_root / "model_cache.sqlite").write_text("project_db", encoding="utf-8")
|
||||
|
||||
manager.set("use_portable_settings", False)
|
||||
|
||||
assert manager.settings_file == str(user_settings)
|
||||
assert (user_dir / "model_cache" / "project_marker.txt").read_text(encoding="utf-8") == "project_marker"
|
||||
assert (user_dir / "model_cache.sqlite").read_text(encoding="utf-8") == "project_db"
|
||||
|
||||
|
||||
def test_download_path_template_parses_json_string(manager):
|
||||
templates = {"lora": "{author}", "checkpoint": "{author}", "embedding": "{author}"}
|
||||
manager.settings["download_path_templates"] = json.dumps(templates)
|
||||
@@ -212,8 +275,7 @@ def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
||||
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
thread = threading.Thread(target=loop.run_forever, daemon=True)
|
||||
thread.start()
|
||||
loop._thread_id = 1
|
||||
|
||||
class DummyScanner:
|
||||
def __init__(self):
|
||||
@@ -227,12 +289,16 @@ def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
||||
|
||||
dispatched_loops = []
|
||||
futures = []
|
||||
original_run_coroutine_threadsafe = asyncio.run_coroutine_threadsafe
|
||||
|
||||
def tracking_run_coroutine_threadsafe(coro, target_loop):
|
||||
dispatched_loops.append(target_loop)
|
||||
future = original_run_coroutine_threadsafe(coro, target_loop)
|
||||
future = Future()
|
||||
futures.append(future)
|
||||
try:
|
||||
result = asyncio.run(coro)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
else:
|
||||
future.set_result(result)
|
||||
return future
|
||||
|
||||
def fake_get_service_sync(cls, name):
|
||||
@@ -254,8 +320,7 @@ def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
|
||||
assert dummy_scanner.calls == ["file_name"]
|
||||
assert dispatched_loops == [dummy_scanner.loop]
|
||||
finally:
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
thread.join(timeout=1)
|
||||
loop._thread_id = None
|
||||
loop.close()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user