feat: add app-level proxy settings with UI integration and session management, fixes #382

This commit is contained in:
Will Miao
2025-09-12 11:22:32 +08:00
parent beb8ff1dd1
commit e713bd1ca2
16 changed files with 556 additions and 41 deletions

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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}")

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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);
}
}

View File

@@ -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');

View File

@@ -445,6 +445,129 @@
</div>
</div>
<!-- Proxy Settings Section -->
<div class="settings-section">
<h3>{{ t('settings.sections.proxySettings') }}</h3>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyEnabled">{{ t('settings.proxySettings.enableProxy') }}</label>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="proxyEnabled"
onchange="settingsManager.saveToggleSetting('proxyEnabled', 'proxy_enabled')">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.enableProxyHelp') }}
</div>
</div>
<div class="proxy-settings-group" id="proxySettingsGroup" style="display: none;">
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyType">{{ t('settings.proxySettings.proxyType') }}</label>
</div>
<div class="setting-control select-control">
<select id="proxyType" onchange="settingsManager.saveSelectSetting('proxyType', 'proxy_type')">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
<option value="socks4">SOCKS4</option>
<option value="socks5">SOCKS5</option>
</select>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.proxyTypeHelp') }}
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyHost">{{ t('settings.proxySettings.proxyHost') }}</label>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input type="text" id="proxyHost"
placeholder="{{ t('settings.proxySettings.proxyHostPlaceholder') }}"
onblur="settingsManager.saveInputSetting('proxyHost', 'proxy_host')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.proxyHostHelp') }}
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyPort">{{ t('settings.proxySettings.proxyPort') }}</label>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input type="text" id="proxyPort"
placeholder="{{ t('settings.proxySettings.proxyPortPlaceholder') }}"
onblur="settingsManager.saveInputSetting('proxyPort', 'proxy_port')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.proxyPortHelp') }}
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyUsername">{{ t('settings.proxySettings.proxyUsername') }}</label>
</div>
<div class="setting-control">
<div class="text-input-wrapper">
<input type="text" id="proxyUsername"
placeholder="{{ t('settings.proxySettings.proxyUsernamePlaceholder') }}"
onblur="settingsManager.saveInputSetting('proxyUsername', 'proxy_username')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
</div>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.proxyUsernameHelp') }}
</div>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="proxyPassword">{{ t('settings.proxySettings.proxyPassword') }}</label>
</div>
<div class="setting-control">
<div class="api-key-input">
<input type="password" id="proxyPassword"
placeholder="{{ t('settings.proxySettings.proxyPasswordPlaceholder') }}"
onblur="settingsManager.saveInputSetting('proxyPassword', 'proxy_password')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
<button class="toggle-visibility">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div>
<div class="input-help">
{{ t('settings.proxySettings.proxyPasswordHelp') }}
</div>
</div>
</div>
</div>
<!-- Misc. Section -->
<div class="settings-section">
<h3>{{ t('settings.sections.misc') }}</h3>
@@ -466,6 +589,7 @@
</div>
</div>
</div>
</div>
</div>
</div>