diff --git a/locales/de.json b/locales/de.json index 49596c76..f0125334 100644 --- a/locales/de.json +++ b/locales/de.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "Download-Pfad-Vorlagen", "exampleImages": "Beispielbilder", "misc": "Verschiedenes", - "metadataArchive": "Metadaten-Archiv-Datenbank" + "metadataArchive": "Metadaten-Archiv-Datenbank", + "proxySettings": "Proxy-Einstellungen" }, "contentFiltering": { "blurNsfwContent": "NSFW-Inhalte unscharf stellen", @@ -300,6 +301,24 @@ "connecting": "Verbindung zum Download-Server wird hergestellt...", "completed": "Abgeschlossen", "downloadComplete": "Download erfolgreich abgeschlossen" + }, + "proxySettings": { + "enableProxy": "App-Proxy aktivieren", + "enableProxyHelp": "Aktivieren Sie benutzerdefinierte Proxy-Einstellungen für diese Anwendung. Überschreibt die System-Proxy-Einstellungen.", + "proxyType": "Proxy-Typ", + "proxyTypeHelp": "Wählen Sie den Typ des Proxy-Servers (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "Proxy-Host", + "proxyHostPlaceholder": "proxy.beispiel.de", + "proxyHostHelp": "Der Hostname oder die IP-Adresse Ihres Proxy-Servers", + "proxyPort": "Proxy-Port", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "Die Portnummer Ihres Proxy-Servers", + "proxyUsername": "Benutzername (optional)", + "proxyUsernamePlaceholder": "benutzername", + "proxyUsernameHelp": "Benutzername für die Proxy-Authentifizierung (falls erforderlich)", + "proxyPassword": "Passwort (optional)", + "proxyPasswordPlaceholder": "passwort", + "proxyPasswordHelp": "Passwort für die Proxy-Authentifizierung (falls erforderlich)" } }, "loras": { diff --git a/locales/en.json b/locales/en.json index 58053fca..24e4704e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "Download Path Templates", "exampleImages": "Example Images", "misc": "Misc.", - "metadataArchive": "Metadata Archive Database" + "metadataArchive": "Metadata Archive Database", + "proxySettings": "Proxy Settings" }, "contentFiltering": { "blurNsfwContent": "Blur NSFW Content", @@ -300,6 +301,24 @@ "connecting": "Connecting to download server...", "completed": "Completed", "downloadComplete": "Download completed successfully" + }, + "proxySettings": { + "enableProxy": "Enable App-level Proxy", + "enableProxyHelp": "Enable custom proxy settings for this application, overriding system proxy settings", + "proxyType": "Proxy Type", + "proxyTypeHelp": "Select the type of proxy server (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "Proxy Host", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "The hostname or IP address of your proxy server", + "proxyPort": "Proxy Port", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "The port number of your proxy server", + "proxyUsername": "Username (Optional)", + "proxyUsernamePlaceholder": "username", + "proxyUsernameHelp": "Username for proxy authentication (if required)", + "proxyPassword": "Password (Optional)", + "proxyPasswordPlaceholder": "password", + "proxyPasswordHelp": "Password for proxy authentication (if required)" } }, "loras": { diff --git a/locales/es.json b/locales/es.json index 9072722c..a3e04cd8 100644 --- a/locales/es.json +++ b/locales/es.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "Plantillas de rutas de descarga", "exampleImages": "Imágenes de ejemplo", "misc": "Varios", - "metadataArchive": "Base de datos de archivo de metadatos" + "metadataArchive": "Base de datos de archivo de metadatos", + "proxySettings": "Configuración de proxy" }, "contentFiltering": { "blurNsfwContent": "Difuminar contenido NSFW", @@ -300,6 +301,24 @@ "connecting": "Conectando al servidor de descarga...", "completed": "Completado", "downloadComplete": "Descarga completada exitosamente" + }, + "proxySettings": { + "enableProxy": "Habilitar proxy a nivel de aplicación", + "enableProxyHelp": "Habilita la configuración de proxy personalizada para esta aplicación, sobrescribiendo la configuración de proxy del sistema", + "proxyType": "Tipo de proxy", + "proxyTypeHelp": "Selecciona el tipo de servidor proxy (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "Host del proxy", + "proxyHostPlaceholder": "proxy.ejemplo.com", + "proxyHostHelp": "El nombre de host o dirección IP de tu servidor proxy", + "proxyPort": "Puerto del proxy", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "El número de puerto de tu servidor proxy", + "proxyUsername": "Usuario (opcional)", + "proxyUsernamePlaceholder": "usuario", + "proxyUsernameHelp": "Usuario para autenticación de proxy (si es necesario)", + "proxyPassword": "Contraseña (opcional)", + "proxyPasswordPlaceholder": "contraseña", + "proxyPasswordHelp": "Contraseña para autenticación de proxy (si es necesario)" } }, "loras": { diff --git a/locales/fr.json b/locales/fr.json index 16e3e3d7..38a6b1f5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "Modèles de chemin de téléchargement", "exampleImages": "Images d'exemple", "misc": "Divers", - "metadataArchive": "Base de données d'archive des métadonnées" + "metadataArchive": "Base de données d'archive des métadonnées", + "proxySettings": "Paramètres du proxy" }, "contentFiltering": { "blurNsfwContent": "Flouter le contenu NSFW", @@ -300,6 +301,24 @@ "connecting": "Connexion au serveur de téléchargement...", "completed": "Terminé", "downloadComplete": "Téléchargement terminé avec succès" + }, + "proxySettings": { + "enableProxy": "Activer le proxy au niveau de l'application", + "enableProxyHelp": "Activer les paramètres de proxy personnalisés pour cette application, remplaçant les paramètres de proxy système", + "proxyType": "Type de proxy", + "proxyTypeHelp": "Sélectionnez le type de serveur proxy (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "Hôte du proxy", + "proxyHostPlaceholder": "proxy.exemple.com", + "proxyHostHelp": "Le nom d'hôte ou l'adresse IP de votre serveur proxy", + "proxyPort": "Port du proxy", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "Le numéro de port de votre serveur proxy", + "proxyUsername": "Nom d'utilisateur (optionnel)", + "proxyUsernamePlaceholder": "nom_utilisateur", + "proxyUsernameHelp": "Nom d'utilisateur pour l'authentification proxy (si nécessaire)", + "proxyPassword": "Mot de passe (optionnel)", + "proxyPasswordPlaceholder": "mot_de_passe", + "proxyPasswordHelp": "Mot de passe pour l'authentification proxy (si nécessaire)" } }, "loras": { diff --git a/locales/ja.json b/locales/ja.json index 90ab446d..f8bdefc6 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "ダウンロードパステンプレート", "exampleImages": "例画像", "misc": "その他", - "metadataArchive": "メタデータアーカイブデータベース" + "metadataArchive": "メタデータアーカイブデータベース", + "proxySettings": "プロキシ設定" }, "contentFiltering": { "blurNsfwContent": "NSFWコンテンツをぼかす", @@ -300,6 +301,24 @@ "connecting": "ダウンロードサーバーに接続中...", "completed": "完了", "downloadComplete": "ダウンロードが正常に完了しました" + }, + "proxySettings": { + "enableProxy": "アプリレベルのプロキシを有効化", + "enableProxyHelp": "このアプリケーション専用のカスタムプロキシ設定を有効にします(システムのプロキシ設定を上書きします)", + "proxyType": "プロキシタイプ", + "proxyTypeHelp": "プロキシサーバーの種類を選択(HTTP、HTTPS、SOCKS4、SOCKS5)", + "proxyHost": "プロキシホスト", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "プロキシサーバーのホスト名またはIPアドレス", + "proxyPort": "プロキシポート", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "プロキシサーバーのポート番号", + "proxyUsername": "ユーザー名(任意)", + "proxyUsernamePlaceholder": "ユーザー名", + "proxyUsernameHelp": "プロキシ認証用のユーザー名(必要な場合)", + "proxyPassword": "パスワード(任意)", + "proxyPasswordPlaceholder": "パスワード", + "proxyPasswordHelp": "プロキシ認証用のパスワード(必要な場合)" } }, "loras": { diff --git a/locales/ko.json b/locales/ko.json index d180b076..08ab8e38 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "다운로드 경로 템플릿", "exampleImages": "예시 이미지", "misc": "기타", - "metadataArchive": "메타데이터 아카이브 데이터베이스" + "metadataArchive": "메타데이터 아카이브 데이터베이스", + "proxySettings": "프록시 설정" }, "contentFiltering": { "blurNsfwContent": "NSFW 콘텐츠 블러 처리", @@ -300,6 +301,24 @@ "connecting": "다운로드 서버에 연결 중...", "completed": "완료됨", "downloadComplete": "다운로드가 성공적으로 완료되었습니다" + }, + "proxySettings": { + "enableProxy": "앱 수준 프록시 활성화", + "enableProxyHelp": "이 애플리케이션에 대한 사용자 지정 프록시 설정을 활성화하여 시스템 프록시 설정을 무시합니다", + "proxyType": "프록시 유형", + "proxyTypeHelp": "프록시 서버 유형을 선택하세요 (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "프록시 호스트", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "프록시 서버의 호스트명 또는 IP 주소", + "proxyPort": "프록시 포트", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "프록시 서버의 포트 번호", + "proxyUsername": "사용자 이름 (선택사항)", + "proxyUsernamePlaceholder": "username", + "proxyUsernameHelp": "프록시 인증에 필요한 사용자 이름 (필요한 경우)", + "proxyPassword": "비밀번호 (선택사항)", + "proxyPasswordPlaceholder": "password", + "proxyPasswordHelp": "프록시 인증에 필요한 비밀번호 (필요한 경우)" } }, "loras": { diff --git a/locales/ru.json b/locales/ru.json index 6850954a..522ac612 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "Шаблоны путей загрузки", "exampleImages": "Примеры изображений", "misc": "Разное", - "metadataArchive": "Архив метаданных" + "metadataArchive": "Архив метаданных", + "proxySettings": "Настройки прокси" }, "contentFiltering": { "blurNsfwContent": "Размывать NSFW контент", @@ -300,6 +301,24 @@ "connecting": "Подключение к серверу загрузки...", "completed": "Завершено", "downloadComplete": "Загрузка успешно завершена" + }, + "proxySettings": { + "enableProxy": "Включить прокси на уровне приложения", + "enableProxyHelp": "Включить пользовательские настройки прокси для этого приложения, переопределяя системные настройки прокси", + "proxyType": "Тип прокси", + "proxyTypeHelp": "Выберите тип прокси-сервера (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "Хост прокси", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "Имя хоста или IP-адрес вашего прокси-сервера", + "proxyPort": "Порт прокси", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "Номер порта вашего прокси-сервера", + "proxyUsername": "Имя пользователя (необязательно)", + "proxyUsernamePlaceholder": "имя пользователя", + "proxyUsernameHelp": "Имя пользователя для аутентификации на прокси (если требуется)", + "proxyPassword": "Пароль (необязательно)", + "proxyPasswordPlaceholder": "пароль", + "proxyPasswordHelp": "Пароль для аутентификации на прокси (если требуется)" } }, "loras": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index bab32fd9..5884f1fc 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "下载路径模板", "exampleImages": "示例图片", "misc": "其他", - "metadataArchive": "元数据归档数据库" + "metadataArchive": "元数据归档数据库", + "proxySettings": "代理设置" }, "contentFiltering": { "blurNsfwContent": "模糊 NSFW 内容", @@ -300,6 +301,24 @@ "connecting": "正在连接下载服务器...", "completed": "已完成", "downloadComplete": "下载成功完成" + }, + "proxySettings": { + "enableProxy": "启用应用级代理", + "enableProxyHelp": "为此应用启用自定义代理设置,覆盖系统代理设置", + "proxyType": "代理类型", + "proxyTypeHelp": "选择代理服务器类型 (HTTP, HTTPS, SOCKS4, SOCKS5)", + "proxyHost": "代理主机", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "代理服务器的主机名或IP地址", + "proxyPort": "代理端口", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "代理服务器的端口号", + "proxyUsername": "用户名 (可选)", + "proxyUsernamePlaceholder": "用户名", + "proxyUsernameHelp": "代理认证的用户名 (如果需要)", + "proxyPassword": "密码 (可选)", + "proxyPasswordPlaceholder": "密码", + "proxyPasswordHelp": "代理认证的密码 (如果需要)" } }, "loras": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 926b6888..5e982e5f 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -181,7 +181,8 @@ "downloadPathTemplates": "下載路徑範本", "exampleImages": "範例圖片", "misc": "其他", - "metadataArchive": "中繼資料封存資料庫" + "metadataArchive": "中繼資料封存資料庫", + "proxySettings": "代理設定" }, "contentFiltering": { "blurNsfwContent": "模糊 NSFW 內容", @@ -300,6 +301,24 @@ "connecting": "正在連接下載伺服器...", "completed": "已完成", "downloadComplete": "下載成功完成" + }, + "proxySettings": { + "enableProxy": "啟用應用程式代理", + "enableProxyHelp": "啟用此應用程式的自訂代理設定,將覆蓋系統代理設定", + "proxyType": "代理類型", + "proxyTypeHelp": "選擇代理伺服器類型(HTTP、HTTPS、SOCKS4、SOCKS5)", + "proxyHost": "代理主機", + "proxyHostPlaceholder": "proxy.example.com", + "proxyHostHelp": "您的代理伺服器主機名稱或 IP 位址", + "proxyPort": "代理埠號", + "proxyPortPlaceholder": "8080", + "proxyPortHelp": "您的代理伺服器埠號", + "proxyUsername": "使用者名稱(選填)", + "proxyUsernamePlaceholder": "username", + "proxyUsernameHelp": "代理驗證所需的使用者名稱(如有需要)", + "proxyPassword": "密碼(選填)", + "proxyPasswordPlaceholder": "password", + "proxyPasswordHelp": "代理驗證所需的密碼(如有需要)" } }, "loras": { diff --git a/py/lora_manager.py b/py/lora_manager.py index f6598bca..96bae4de 100644 --- a/py/lora_manager.py +++ b/py/lora_manager.py @@ -409,9 +409,6 @@ class LoraManager: await cls._remove_folder_safely(folder_path) invalid_hash_folders_removed += 1 continue - - logger.debug(f"Keeping valid example images folder: {folder_name}") - except Exception as e: logger.error(f"Error processing example images folder {folder_name}: {e}") diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 5ae2eadd..013923b7 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -14,6 +14,7 @@ from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS, NODE_TYPES, DEFAULT_NO from ..services.service_registry import ServiceRegistry from ..services.metadata_service import get_metadata_archive_manager, update_metadata_providers from ..services.websocket_manager import ws_manager +from ..services.downloader import get_downloader logger = logging.getLogger(__name__) standalone_mode = 'nodes' not in sys.modules @@ -123,6 +124,8 @@ class MiscRoutes: """Update application settings""" try: data = await request.json() + proxy_keys = {'proxy_enabled', 'proxy_host', 'proxy_port', 'proxy_username', 'proxy_password', 'proxy_type'} + proxy_changed = False # Validate and update settings for key, value in data.items(): @@ -142,11 +145,22 @@ class MiscRoutes: if old_path != value: logger.info(f"Example images path changed to {value} - server restart required") - # Save to settings - settings.set(key, value) + # Handle deletion for proxy credentials + if value == '__DELETE__' and key in ('proxy_username', 'proxy_password'): + settings.delete(key) + else: + # Save to settings + settings.set(key, value) if key == 'enable_metadata_archive_db': await update_metadata_providers() + + if key in proxy_keys: + proxy_changed = True + + if proxy_changed: + downloader = await get_downloader() + await downloader.refresh_session() return web.json_response({'success': True}) except Exception as e: diff --git a/py/services/downloader.py b/py/services/downloader.py index dc38c0d1..4a26c38b 100644 --- a/py/services/downloader.py +++ b/py/services/downloader.py @@ -45,6 +45,7 @@ class Downloader: # Session management self._session = None self._session_created_at = None + self._proxy_url = None # Store proxy URL for current session # Configuration self.chunk_size = 4 * 1024 * 1024 # 4MB chunks for better throughput @@ -64,6 +65,13 @@ class Downloader: await self._create_session() return self._session + @property + def proxy_url(self) -> Optional[str]: + """Get the current proxy URL (initialize if needed)""" + if not hasattr(self, '_proxy_url'): + self._proxy_url = None + return self._proxy_url + def _should_refresh_session(self) -> bool: """Check if session should be refreshed""" if self._session is None: @@ -84,6 +92,26 @@ class Downloader: if self._session is not None: await self._session.close() + # Check for app-level proxy settings + proxy_url = None + if settings.get('proxy_enabled', False): + proxy_host = settings.get('proxy_host', '').strip() + proxy_port = settings.get('proxy_port', '').strip() + proxy_type = settings.get('proxy_type', 'http').lower() + proxy_username = settings.get('proxy_username', '').strip() + proxy_password = settings.get('proxy_password', '').strip() + + if proxy_host and proxy_port: + # Build proxy URL + if proxy_username and proxy_password: + proxy_url = f"{proxy_type}://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}" + else: + proxy_url = f"{proxy_type}://{proxy_host}:{proxy_port}" + + logger.info(f"Using app-level proxy: {proxy_type}://{proxy_host}:{proxy_port}") + logger.debug("Proxy mode: app-level proxy is active.") + else: + logger.debug("Proxy mode: system-level proxy (trust_env) will be used if configured in environment.") # Optimize TCP connection parameters connector = aiohttp.TCPConnector( ssl=True, @@ -102,12 +130,15 @@ class Downloader: self._session = aiohttp.ClientSession( connector=connector, - trust_env=True, # Use system proxy settings + trust_env=proxy_url is None, # Only use system proxy if no app-level proxy is set timeout=timeout ) + + # Store proxy URL for use in requests + self._proxy_url = proxy_url self._session_created_at = datetime.now() - logger.debug("Created new HTTP session") + logger.debug("Created new HTTP session with proxy settings. App-level proxy: %s, System-level proxy (trust_env): %s", bool(proxy_url), proxy_url is None) def _get_auth_headers(self, use_auth: bool = False) -> Dict[str, str]: """Get headers with optional authentication""" @@ -164,6 +195,11 @@ class Downloader: while retry_count <= self.max_retries: try: session = await self.session + # Debug log for proxy mode at request time + if self.proxy_url: + logger.debug(f"[download_file] Using app-level proxy: {self.proxy_url}") + else: + logger.debug("[download_file] Using system-level proxy (trust_env) if configured.") # Add Range header for resume if we have partial data request_headers = headers.copy() @@ -177,7 +213,7 @@ class Downloader: if resume_offset > 0: logger.debug(f"Requesting range from byte {resume_offset}") - async with session.get(url, headers=request_headers, allow_redirects=True) as response: + async with session.get(url, headers=request_headers, allow_redirects=True, proxy=self.proxy_url) as response: # Handle different response codes if response.status == 200: # Full content response @@ -202,7 +238,7 @@ class Downloader: part_size = os.path.getsize(part_path) logger.warning(f"Range not satisfiable. Part file size: {part_size}") # Try to get actual file size - head_response = await session.head(url, headers=headers) + head_response = await session.head(url, headers=headers, proxy=self.proxy_url) if head_response.status == 200: actual_size = int(head_response.headers.get('content-length', 0)) if part_size == actual_size: @@ -345,13 +381,18 @@ class Downloader: """ try: session = await self.session + # Debug log for proxy mode at request time + if self.proxy_url: + logger.debug(f"[download_to_memory] Using app-level proxy: {self.proxy_url}") + else: + logger.debug("[download_to_memory] Using system-level proxy (trust_env) if configured.") # Prepare headers headers = self._get_auth_headers(use_auth) if custom_headers: headers.update(custom_headers) - async with session.get(url, headers=headers) as response: + async with session.get(url, headers=headers, proxy=self.proxy_url) as response: if response.status == 200: content = await response.read() return True, content @@ -387,13 +428,18 @@ class Downloader: """ try: session = await self.session + # Debug log for proxy mode at request time + if self.proxy_url: + logger.debug(f"[get_response_headers] Using app-level proxy: {self.proxy_url}") + else: + logger.debug("[get_response_headers] Using system-level proxy (trust_env) if configured.") # Prepare headers headers = self._get_auth_headers(use_auth) if custom_headers: headers.update(custom_headers) - async with session.head(url, headers=headers) as response: + async with session.head(url, headers=headers, proxy=self.proxy_url) as response: if response.status == 200: return True, dict(response.headers) else: @@ -426,12 +472,21 @@ class Downloader: """ try: session = await self.session + # Debug log for proxy mode at request time + if self.proxy_url: + logger.debug(f"[make_request] Using app-level proxy: {self.proxy_url}") + else: + logger.debug("[make_request] Using system-level proxy (trust_env) if configured.") # Prepare headers headers = self._get_auth_headers(use_auth) if custom_headers: headers.update(custom_headers) + # Add proxy to kwargs if not already present + if 'proxy' not in kwargs: + kwargs['proxy'] = self.proxy_url + async with session.request(method, url, headers=headers, **kwargs) as response: if response.status == 200: # Try to parse as JSON, fall back to text @@ -460,7 +515,13 @@ class Downloader: await self._session.close() self._session = None self._session_created_at = None + self._proxy_url = None logger.debug("Closed HTTP session") + + async def refresh_session(self): + """Force refresh the HTTP session (useful when proxy settings change)""" + await self._create_session() + logger.info("HTTP session refreshed due to settings change") # Global instance accessor diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 7d99da48..5a95e58e 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -82,7 +82,13 @@ class SettingsManager: "civitai_api_key": "", "show_only_sfw": False, "language": "en", - "enable_metadata_archive_db": False # Enable metadata archive database + "enable_metadata_archive_db": False, # Enable metadata archive database + "proxy_enabled": False, # Enable app-level proxy + "proxy_host": "", # Proxy host + "proxy_port": "", # Proxy port + "proxy_username": "", # Proxy username (optional) + "proxy_password": "", # Proxy password (optional) + "proxy_type": "http" # Proxy type: http, https, socks4, socks5 } def get(self, key: str, default: Any = None) -> Any: @@ -94,6 +100,13 @@ class SettingsManager: self.settings[key] = value self._save_settings() + def delete(self, key: str) -> None: + """Delete setting key and save""" + if key in self.settings: + del self.settings[key] + self._save_settings() + logger.info(f"Deleted setting: {key}") + def _save_settings(self) -> None: """Save settings to file""" try: diff --git a/static/css/components/modal/settings-modal.css b/static/css/components/modal/settings-modal.css index a13dc856..3f59203e 100644 --- a/static/css/components/modal/settings-modal.css +++ b/static/css/components/modal/settings-modal.css @@ -101,7 +101,7 @@ .api-key-input input { width: 100%; padding: 6px 40px 6px 10px; /* Add left padding */ - height: 32px; + height: 20px; border-radius: var(--border-radius-xs); border: 1px solid var(--border-color); background-color: var(--lora-surface); @@ -123,6 +123,36 @@ opacity: 1; } +/* Text input wrapper styles for consistent input styling */ +.text-input-wrapper { + width: 100%; + position: relative; + display: flex; + align-items: center; +} + +.text-input-wrapper input { + width: 100%; + padding: 6px 10px; + height: 20px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + background-color: var(--lora-surface); + color: var(--text-color); + font-size: 0.95em; +} + +.text-input-wrapper input:focus { + border-color: var(--lora-accent); + outline: none; + box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1); +} + +/* Dark theme specific adjustments */ +[data-theme="dark"] .text-input-wrapper input { + background-color: rgba(30, 30, 30, 0.9); +} + .input-help { font-size: 0.85em; color: var(--text-color); @@ -312,7 +342,7 @@ input:checked + .toggle-slider:before { border-radius: var(--border-radius-xs); border: 1px solid var(--border-color); background-color: var(--lora-surface); - color: var (--text-color); + color: var(--text-color); font-size: 0.95em; height: 32px; } @@ -571,10 +601,31 @@ input:checked + .toggle-slider:before { background-color: rgba(30, 30, 30, 0.9); } +/* Proxy Settings Styles */ +.proxy-settings-group { + margin-left: var(--space-1); + padding-left: var(--space-1); + border-left: 2px solid var(--lora-border); + animation: slideDown 0.3s ease-out; +} + +.proxy-settings-group .setting-item { + margin-bottom: var(--space-2); +} + /* Responsive adjustments */ @media (max-width: 768px) { .placeholder-info { flex-direction: column; align-items: flex-start; } + + .proxy-settings-group { + margin-left: 0; + padding-left: var(--space-1); + border-left: none; + border-top: 1px solid var(--lora-border); + padding-top: var(--space-2); + margin-top: var(--space-2); + } } \ No newline at end of file diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 2d477d24..96e3064d 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -125,7 +125,13 @@ export class SettingsManager { 'default_checkpoint_root', 'default_embedding_root', 'base_model_path_mappings', - 'download_path_templates' + 'download_path_templates', + 'proxy_enabled', + 'proxy_type', + 'proxy_host', + 'proxy_port', + 'proxy_username', + 'proxy_password' ]; // Build payload for syncing @@ -281,6 +287,60 @@ export class SettingsManager { const currentLanguage = state.global.settings.language || 'en'; languageSelect.value = currentLanguage; } + + this.loadProxySettings(); + } + + loadProxySettings() { + // Load proxy enabled setting + const proxyEnabledCheckbox = document.getElementById('proxyEnabled'); + if (proxyEnabledCheckbox) { + proxyEnabledCheckbox.checked = state.global.settings.proxy_enabled || false; + + // Add event listener for toggling proxy settings group visibility + proxyEnabledCheckbox.addEventListener('change', () => { + const proxySettingsGroup = document.getElementById('proxySettingsGroup'); + if (proxySettingsGroup) { + proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none'; + } + }); + + // Set initial visibility + const proxySettingsGroup = document.getElementById('proxySettingsGroup'); + if (proxySettingsGroup) { + proxySettingsGroup.style.display = proxyEnabledCheckbox.checked ? 'block' : 'none'; + } + } + + // Load proxy type + const proxyTypeSelect = document.getElementById('proxyType'); + if (proxyTypeSelect) { + proxyTypeSelect.value = state.global.settings.proxy_type || 'http'; + } + + // Load proxy host + const proxyHostInput = document.getElementById('proxyHost'); + if (proxyHostInput) { + proxyHostInput.value = state.global.settings.proxy_host || ''; + } + + // Load proxy port + const proxyPortInput = document.getElementById('proxyPort'); + if (proxyPortInput) { + proxyPortInput.value = state.global.settings.proxy_port || ''; + } + + // Load proxy username + const proxyUsernameInput = document.getElementById('proxyUsername'); + if (proxyUsernameInput) { + proxyUsernameInput.value = state.global.settings.proxy_username || ''; + } + + // Load proxy password + const proxyPasswordInput = document.getElementById('proxyPassword'); + if (proxyPasswordInput) { + proxyPasswordInput.value = state.global.settings.proxy_password || ''; + } } async loadLoraRoots() { @@ -791,6 +851,14 @@ export class SettingsManager { state.global.settings.includeTriggerWords = value; } else if (settingKey === 'enable_metadata_archive_db') { state.global.settings.enable_metadata_archive_db = value; + } else if (settingKey === 'proxy_enabled') { + state.global.settings.proxy_enabled = value; + + // Toggle visibility of proxy settings group + const proxySettingsGroup = document.getElementById('proxySettingsGroup'); + if (proxySettingsGroup) { + proxySettingsGroup.style.display = value ? 'block' : 'none'; + } } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -801,7 +869,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (['show_only_sfw', 'enable_metadata_archive_db'].includes(settingKey)) { + if (['show_only_sfw', 'enable_metadata_archive_db', 'proxy_enabled'].includes(settingKey)) { const payload = {}; payload[settingKey] = value; @@ -879,6 +947,8 @@ export class SettingsManager { state.global.settings.compactMode = (value !== 'default'); } else if (settingKey === 'card_info_display') { state.global.settings.cardInfoDisplay = value; + } else if (settingKey === 'proxy_type') { + state.global.settings.proxy_type = value; } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -889,7 +959,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates') { + if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates' || settingKey.startsWith('proxy_')) { const payload = {}; if (settingKey === 'download_path_templates') { payload[settingKey] = state.global.settings.download_path_templates; @@ -1183,7 +1253,7 @@ export class SettingsManager { const element = document.getElementById(elementId); if (!element) return; - const value = element.value; + const value = element.value.trim(); // Trim whitespace // For API key or other inputs that need to be saved on backend try { @@ -1193,25 +1263,39 @@ export class SettingsManager { return; // No change, exit early } - // Update state - state.global.settings[settingKey] = value; + // For username and password, remove the setting if value is empty + if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') { + // Remove from state instead of setting to empty string + delete state.global.settings[settingKey]; + } else { + // Update state with value (including empty strings for non-optional fields) + state.global.settings[settingKey] = value; + } setStorageItem('settings', state.global.settings); // For backend settings, make API call - const payload = {}; - payload[settingKey] = value; - - const response = await fetch('/api/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload) - }); + if (settingKey === 'civitai_api_key' || settingKey.startsWith('proxy_')) { + const payload = {}; + + // For username and password, send delete flag if empty to remove from backend + if ((settingKey === 'proxy_username' || settingKey === 'proxy_password') && value === '') { + payload[settingKey] = '__DELETE__'; + } else { + payload[settingKey] = value; + } + + const response = await fetch('/api/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); - if (!response.ok) { - throw new Error('Failed to save setting'); + if (!response.ok) { + throw new Error('Failed to save setting'); + } } showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index 3b1b3a8a..09fea0c9 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -445,6 +445,129 @@ + +
+

{{ t('settings.sections.proxySettings') }}

+ +
+
+
+ +
+
+ +
+
+
+ {{ t('settings.proxySettings.enableProxyHelp') }} +
+
+ + +
+

{{ t('settings.sections.misc') }}

@@ -466,6 +589,7 @@
+ \ No newline at end of file