mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-22 11:21:15 -03:00
Compare commits
5 Commits
v1.1.3
...
258b2622d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
258b2622d5 | ||
|
|
80ec9085dd | ||
|
|
c5c7373e10 | ||
|
|
b7721866e5 | ||
|
|
8314b9bedb |
@@ -251,7 +251,18 @@
|
||||
"toggle": "Theme wechseln",
|
||||
"switchToLight": "Zu hellem Theme wechseln",
|
||||
"switchToDark": "Zu dunklem Theme wechseln",
|
||||
"switchToAuto": "Zu automatischem Theme wechseln"
|
||||
"switchToAuto": "Zu automatischem Theme wechseln",
|
||||
"presets": "Theme-Voreinstellungen",
|
||||
"default": "Standard",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "Modus",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "Updates prüfen",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "Toggle theme",
|
||||
"switchToLight": "Switch to light theme",
|
||||
"switchToDark": "Switch to dark theme",
|
||||
"switchToAuto": "Switch to auto theme"
|
||||
"switchToAuto": "Switch to auto theme",
|
||||
"presets": "Theme Presets",
|
||||
"default": "Default",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "Mode",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "Check Updates",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "Cambiar tema",
|
||||
"switchToLight": "Cambiar a tema claro",
|
||||
"switchToDark": "Cambiar a tema oscuro",
|
||||
"switchToAuto": "Cambiar a tema automático"
|
||||
"switchToAuto": "Cambiar a tema automático",
|
||||
"presets": "Preajustes de tema",
|
||||
"default": "Predeterminado",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "Modo",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "Comprobar actualizaciones",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "Basculer le thème",
|
||||
"switchToLight": "Passer au thème clair",
|
||||
"switchToDark": "Passer au thème sombre",
|
||||
"switchToAuto": "Passer au thème automatique"
|
||||
"switchToAuto": "Passer au thème automatique",
|
||||
"presets": "Préréglages de thème",
|
||||
"default": "Par défaut",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "Mode",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "Vérifier les mises à jour",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "החלף ערכת נושא",
|
||||
"switchToLight": "עבור לערכת נושא בהירה",
|
||||
"switchToDark": "עבור לערכת נושא כהה",
|
||||
"switchToAuto": "עבור לערכת נושא אוטומטית"
|
||||
"switchToAuto": "עבור לערכת נושא אוטומטית",
|
||||
"presets": "ערכות נושא מוגדרות",
|
||||
"default": "ברירת מחדל",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "מצב",
|
||||
"light": "בהיר",
|
||||
"dark": "כהה",
|
||||
"auto": "אוטומטי"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "בדוק עדכונים",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "テーマの切り替え",
|
||||
"switchToLight": "ライトテーマに切り替え",
|
||||
"switchToDark": "ダークテーマに切り替え",
|
||||
"switchToAuto": "自動テーマに切り替え"
|
||||
"switchToAuto": "自動テーマに切り替え",
|
||||
"presets": "テーマプリセット",
|
||||
"default": "デフォルト",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "モード",
|
||||
"light": "ライト",
|
||||
"dark": "ダーク",
|
||||
"auto": "自動"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "更新確認",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "테마 토글",
|
||||
"switchToLight": "라이트 테마로 전환",
|
||||
"switchToDark": "다크 테마로 전환",
|
||||
"switchToAuto": "자동 테마로 전환"
|
||||
"switchToAuto": "자동 테마로 전환",
|
||||
"presets": "테마 프리셋",
|
||||
"default": "기본",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "모드",
|
||||
"light": "라이트",
|
||||
"dark": "다크",
|
||||
"auto": "자동"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "업데이트 확인",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "Переключить тему",
|
||||
"switchToLight": "Переключить на светлую тему",
|
||||
"switchToDark": "Переключить на тёмную тему",
|
||||
"switchToAuto": "Переключить на автоматическую тему"
|
||||
"switchToAuto": "Переключить на автоматическую тему",
|
||||
"presets": "Предустановки тем",
|
||||
"default": "По умолчанию",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "Режим",
|
||||
"light": "Светлый",
|
||||
"dark": "Тёмный",
|
||||
"auto": "Авто"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "Проверить обновления",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "切换主题",
|
||||
"switchToLight": "切换到浅色主题",
|
||||
"switchToDark": "切换到深色主题",
|
||||
"switchToAuto": "切换到自动主题"
|
||||
"switchToAuto": "切换到自动主题",
|
||||
"presets": "主题预设",
|
||||
"default": "默认",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "模式",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"auto": "自动"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "检查更新",
|
||||
|
||||
@@ -251,7 +251,18 @@
|
||||
"toggle": "切換主題",
|
||||
"switchToLight": "切換至淺色主題",
|
||||
"switchToDark": "切換至深色主題",
|
||||
"switchToAuto": "自動主題"
|
||||
"switchToAuto": "自動主題",
|
||||
"presets": "主題預設",
|
||||
"default": "預設",
|
||||
"nord": "Nord",
|
||||
"midnight": "Midnight",
|
||||
"monokai": "Monokai",
|
||||
"dracula": "Dracula",
|
||||
"solarized": "Solarized",
|
||||
"mode": "模式",
|
||||
"light": "淺色",
|
||||
"dark": "深色",
|
||||
"auto": "自動"
|
||||
},
|
||||
"actions": {
|
||||
"checkUpdates": "檢查更新",
|
||||
|
||||
@@ -1820,6 +1820,39 @@ class ModelDownloadHandler:
|
||||
)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def update_download_queue_status(self, request: web.Request) -> web.Response:
|
||||
"""Update the status of a queue item (non-terminal transitions).
|
||||
|
||||
Supported transitions include ``queued → downloading``,
|
||||
``downloading → paused``, ``paused → downloading``, etc.
|
||||
Terminal transitions (``completed``, ``failed``, ``canceled``)
|
||||
should use ``complete_download_in_queue`` instead.
|
||||
"""
|
||||
try:
|
||||
download_id = request.query.get("download_id")
|
||||
status = request.query.get("status")
|
||||
if not download_id or not status:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "download_id and status are required",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
service = await DownloadQueueService.get_instance()
|
||||
updated = await service.update_status(download_id, status)
|
||||
if not updated:
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Download not found in queue"},
|
||||
status=404,
|
||||
)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as exc:
|
||||
self._logger.error(
|
||||
"Error updating download queue status: %s", exc, exc_info=True
|
||||
)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
|
||||
class ModelCivitaiHandler:
|
||||
"""CivitAI integration endpoints."""
|
||||
@@ -2864,6 +2897,7 @@ class ModelHandlerSet:
|
||||
"retry_all_failed_downloads": self.download.retry_all_failed_downloads,
|
||||
"complete_download_in_queue": self.download.complete_download_in_queue,
|
||||
"get_download_stats": self.download.get_download_stats,
|
||||
"update_download_queue_status": self.download.update_download_queue_status,
|
||||
"get_civitai_versions": self.civitai.get_civitai_versions,
|
||||
"get_civitai_model_by_version": self.civitai.get_civitai_model_by_version,
|
||||
"get_civitai_model_by_hash": self.civitai.get_civitai_model_by_hash,
|
||||
|
||||
@@ -138,6 +138,9 @@ COMMON_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/downloads/queue/complete", "complete_download_in_queue"
|
||||
),
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/downloads/queue/status", "update_download_queue_status"
|
||||
),
|
||||
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
||||
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,8 @@ from ..config import config
|
||||
from ..services.settings_manager import get_settings_manager
|
||||
from ..services.server_i18n import server_i18n
|
||||
from ..services.service_registry import ServiceRegistry
|
||||
from ..services.model_query import normalize_sub_type, resolve_sub_type
|
||||
from ..utils.constants import VALID_LORA_SUB_TYPES, VALID_CHECKPOINT_SUB_TYPES
|
||||
from ..utils.usage_stats import UsageStats
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -140,6 +142,21 @@ class StatsRoutes:
|
||||
# Get usage statistics
|
||||
usage_data = await self.usage_stats.get_stats()
|
||||
|
||||
# CivitAI model type distribution across all model types
|
||||
# Use the same logic as the filter panel: normalize_sub_type(resolve_sub_type(entry))
|
||||
# with sub-type validation per model type
|
||||
model_types_counter: Counter[str] = Counter()
|
||||
for entry in lora_cache.raw_data:
|
||||
ntype = normalize_sub_type(resolve_sub_type(entry))
|
||||
if ntype and ntype in VALID_LORA_SUB_TYPES:
|
||||
model_types_counter[ntype] += 1
|
||||
for entry in checkpoint_cache.raw_data:
|
||||
ntype = normalize_sub_type(resolve_sub_type(entry))
|
||||
if ntype and ntype in VALID_CHECKPOINT_SUB_TYPES:
|
||||
model_types_counter[ntype] += 1
|
||||
# Embeddings: always count as "embedding" regardless of CivitAI sub-type
|
||||
model_types_counter['embedding'] = len(embedding_cache.raw_data)
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'data': {
|
||||
@@ -154,7 +171,8 @@ class StatsRoutes:
|
||||
'total_generations': usage_data.get('total_executions', 0),
|
||||
'unused_loras': self._count_unused_models(lora_cache.raw_data, usage_data.get('loras', {})),
|
||||
'unused_checkpoints': self._count_unused_models(checkpoint_cache.raw_data, usage_data.get('checkpoints', {})),
|
||||
'unused_embeddings': self._count_unused_models(embedding_cache.raw_data, usage_data.get('embeddings', {}))
|
||||
'unused_embeddings': self._count_unused_models(embedding_cache.raw_data, usage_data.get('embeddings', {})),
|
||||
'model_types_distribution': dict(model_types_counter.most_common())
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from .metadata_service import get_default_metadata_provider, get_metadata_provid
|
||||
from .downloader import get_downloader, DownloadProgress, DownloadStreamControl
|
||||
from .aria2_downloader import Aria2Error, get_aria2_downloader
|
||||
from .aria2_transfer_state import Aria2TransferStateStore
|
||||
from .download_queue_service import DownloadQueueService
|
||||
|
||||
# Download to temporary file first
|
||||
import tempfile
|
||||
@@ -360,6 +361,15 @@ class DownloadManager:
|
||||
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
|
||||
await self._persist_aria2_state(task_id)
|
||||
|
||||
# Update SQLite queue status to 'downloading'
|
||||
try:
|
||||
queue_service = await DownloadQueueService.get_instance()
|
||||
await queue_service.update_status(task_id, "downloading")
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to update queue status for %s", task_id, exc_info=True
|
||||
)
|
||||
|
||||
# Use original download implementation
|
||||
try:
|
||||
# Check for cancellation before starting
|
||||
@@ -396,6 +406,22 @@ class DownloadManager:
|
||||
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
|
||||
await self._persist_aria2_state(task_id)
|
||||
|
||||
# Move queue item to history on completion
|
||||
try:
|
||||
queue_service = await DownloadQueueService.get_instance()
|
||||
await queue_service.complete_download(
|
||||
download_id=task_id,
|
||||
status=result.get("status", "completed") if result.get("success") else "failed",
|
||||
error=result.get("error") if not result.get("success") else None,
|
||||
file_path=result.get("file_path"),
|
||||
bytes_downloaded=self._active_downloads.get(task_id, {}).get("bytes_downloaded", 0),
|
||||
total_bytes=self._active_downloads.get(task_id, {}).get("total_bytes"),
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to complete queue item for %s", task_id, exc_info=True
|
||||
)
|
||||
|
||||
return result
|
||||
except asyncio.CancelledError:
|
||||
# Handle cancellation
|
||||
@@ -404,6 +430,19 @@ class DownloadManager:
|
||||
self._active_downloads[task_id]["bytes_per_second"] = 0.0
|
||||
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
|
||||
await self._persist_aria2_state(task_id)
|
||||
|
||||
# Move queue item to history as canceled
|
||||
try:
|
||||
queue_service = await DownloadQueueService.get_instance()
|
||||
await queue_service.complete_download(
|
||||
download_id=task_id,
|
||||
status="canceled",
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to cancel queue item for %s", task_id, exc_info=True
|
||||
)
|
||||
|
||||
logger.info(f"Download cancelled for task {task_id}")
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -417,6 +456,22 @@ class DownloadManager:
|
||||
self._active_downloads[task_id]["bytes_per_second"] = 0.0
|
||||
if self._active_downloads[task_id].get("transfer_backend") == "aria2":
|
||||
await self._persist_aria2_state(task_id)
|
||||
|
||||
# Move queue item to history as failed
|
||||
try:
|
||||
queue_service = await DownloadQueueService.get_instance()
|
||||
await queue_service.complete_download(
|
||||
download_id=task_id,
|
||||
status="failed",
|
||||
error=str(e),
|
||||
bytes_downloaded=self._active_downloads.get(task_id, {}).get("bytes_downloaded", 0),
|
||||
total_bytes=self._active_downloads.get(task_id, {}).get("total_bytes"),
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to complete queue item for %s", task_id, exc_info=True
|
||||
)
|
||||
|
||||
return {"success": False, "error": str(e)}
|
||||
finally:
|
||||
# Schedule cleanup of download record after delay
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
}
|
||||
|
||||
.stat-item.success {
|
||||
border-left: 3px solid #00B87A;
|
||||
border-left: 3px solid var(--color-success);
|
||||
}
|
||||
|
||||
.stat-item.failed {
|
||||
@@ -455,7 +455,7 @@
|
||||
|
||||
.results-icon {
|
||||
font-size: 3em;
|
||||
color: #00B87A;
|
||||
color: var(--color-success);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@
|
||||
}
|
||||
|
||||
.result-card.success {
|
||||
border-left: 3px solid #00B87A;
|
||||
border-left: 3px solid var(--color-success);
|
||||
}
|
||||
|
||||
.result-card.failed {
|
||||
@@ -582,8 +582,8 @@
|
||||
}
|
||||
|
||||
.result-item-status.success {
|
||||
background: oklch(from #00B87A l c h / 0.2);
|
||||
color: #00B87A;
|
||||
background: color-mix(in oklch, var(--color-success) 20%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.result-item-status.failed {
|
||||
@@ -661,11 +661,11 @@
|
||||
|
||||
/* Completed State */
|
||||
.batch-progress-container.completed .progress-bar {
|
||||
background: #00B87A;
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.batch-progress-container.completed .status-icon {
|
||||
color: #00B87A;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.batch-progress-container.completed .status-icon i {
|
||||
|
||||
@@ -283,7 +283,6 @@
|
||||
|
||||
.theme-toggle {
|
||||
position: relative;
|
||||
/* Ensure relative positioning for the container */
|
||||
}
|
||||
|
||||
.theme-toggle .light-icon,
|
||||
@@ -293,17 +292,14 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
/* Center perfectly */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* Default state shows dark icon */
|
||||
.theme-toggle .dark-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Light theme shows light icon */
|
||||
.theme-toggle.theme-light .light-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -313,7 +309,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Dark theme shows dark icon */
|
||||
.theme-toggle.theme-dark .dark-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -323,7 +318,6 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Auto theme shows auto icon */
|
||||
.theme-toggle.theme-auto .auto-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -333,6 +327,203 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theme-popover {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: -8px;
|
||||
background: var(--surface-base, #ffffff);
|
||||
border: 1px solid var(--border-base, #e0e0e0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
box-shadow: var(--shadow-xl, 0 4px 16px rgba(0, 0, 0, 0.15));
|
||||
padding: 12px;
|
||||
min-width: 220px;
|
||||
z-index: var(--z-dropdown, 200);
|
||||
animation: theme-popover-in 0.15s ease-out;
|
||||
}
|
||||
|
||||
.theme-popover.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes theme-popover-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-popover-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-popover-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-secondary, #6c757d);
|
||||
}
|
||||
|
||||
.theme-popover-divider {
|
||||
height: 1px;
|
||||
background: var(--border-base, #e0e0e0);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.theme-popover-modes {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.theme-mode-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid var(--border-base, #e0e0e0);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
background: var(--surface-elevated, #ffffff);
|
||||
color: var(--text-primary, #333333);
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
transition: background-color var(--transition-base, 200ms ease),
|
||||
border-color var(--transition-base, 200ms ease),
|
||||
color var(--transition-base, 200ms ease);
|
||||
}
|
||||
|
||||
.theme-mode-btn i {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.theme-mode-btn:hover {
|
||||
background: var(--surface-hover, oklch(95% 0.02 256));
|
||||
border-color: var(--color-accent, oklch(68% 0.28 256));
|
||||
}
|
||||
|
||||
.theme-mode-btn.active {
|
||||
background: var(--color-accent-subtle, oklch(68% 0.28 256 / 0.12));
|
||||
border-color: var(--color-accent, oklch(68% 0.28 256));
|
||||
color: var(--color-accent, oklch(68% 0.28 256));
|
||||
}
|
||||
|
||||
.theme-popover-presets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.theme-preset-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 4px;
|
||||
border: 1px solid var(--border-base, #e0e0e0);
|
||||
border-radius: var(--radius-sm, 6px);
|
||||
background: var(--surface-elevated, #ffffff);
|
||||
color: var(--text-primary, #333333);
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
transition: background-color var(--transition-base, 200ms ease),
|
||||
border-color var(--transition-base, 200ms ease),
|
||||
color var(--transition-base, 200ms ease);
|
||||
}
|
||||
|
||||
.theme-preset-btn:hover {
|
||||
background: var(--surface-hover, oklch(95% 0.02 256));
|
||||
border-color: var(--color-accent, oklch(68% 0.28 256));
|
||||
}
|
||||
|
||||
.theme-preset-btn.active {
|
||||
background: var(--color-accent-subtle, oklch(68% 0.28 256 / 0.12));
|
||||
border-color: var(--color-accent, oklch(68% 0.28 256));
|
||||
color: var(--color-accent, oklch(68% 0.28 256));
|
||||
}
|
||||
|
||||
.preset-swatch {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: var(--radius-xs, 4px);
|
||||
border: 1px solid var(--border-subtle, oklch(72% 0.03 256 / 0.45));
|
||||
flex-shrink: 0;
|
||||
transition: transform var(--transition-base, 200ms ease),
|
||||
box-shadow var(--transition-base, 200ms ease);
|
||||
}
|
||||
|
||||
/* Solid accent colors — each swatch shows the theme's accent color directly.
|
||||
This matches the app's flat, token-driven design language instead of using
|
||||
decorative gradients that clash with the matte aesthetic. */
|
||||
|
||||
.preset-swatch-default {
|
||||
background: oklch(68% 0.28 256);
|
||||
}
|
||||
|
||||
.preset-swatch-nord {
|
||||
background: oklch(62% 0.18 213);
|
||||
}
|
||||
|
||||
.preset-swatch-midnight {
|
||||
background: oklch(52% 0.15 300);
|
||||
}
|
||||
|
||||
.preset-swatch-monokai {
|
||||
background: oklch(72% 0.24 190);
|
||||
}
|
||||
|
||||
.preset-swatch-dracula {
|
||||
background: oklch(68% 0.24 265);
|
||||
}
|
||||
|
||||
.preset-swatch-solarized {
|
||||
background: oklch(55% 0.18 175);
|
||||
}
|
||||
|
||||
.theme-preset-btn.active .preset-swatch {
|
||||
box-shadow: 0 0 0 2px var(--color-accent, oklch(68% 0.28 256));
|
||||
}
|
||||
|
||||
.theme-preset-btn:hover .preset-swatch {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
/* Dark mode: use each preset's dark-mode accent lightness for visibility.
|
||||
These match the --color-accent-l values from [data-theme="dark"][data-theme-preset="..."]
|
||||
in tokens/colors.css so the swatch accurately previews what the theme looks like. */
|
||||
|
||||
[data-theme="dark"] .preset-swatch-default {
|
||||
background: oklch(68% 0.28 256);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .preset-swatch-nord {
|
||||
background: oklch(68% 0.18 213);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .preset-swatch-midnight {
|
||||
background: oklch(68% 0.14 300);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .preset-swatch-monokai {
|
||||
background: oklch(72% 0.24 190);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .preset-swatch-dracula {
|
||||
background: oklch(72% 0.24 265);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .preset-swatch-solarized {
|
||||
background: oklch(60% 0.18 175);
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.update-badge {
|
||||
position: absolute;
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
|
||||
.lora-item.is-early-access {
|
||||
background: rgba(0, 184, 122, 0.05);
|
||||
border-left: 4px solid #00B87A;
|
||||
border-left: 4px solid var(--color-success);
|
||||
}
|
||||
|
||||
.lora-item.missing-locally {
|
||||
@@ -310,7 +310,7 @@
|
||||
|
||||
.missing-lora-item.is-early-access {
|
||||
background: rgba(0, 184, 122, 0.05);
|
||||
border-left: 3px solid #00B87A;
|
||||
border-left: 3px solid var(--color-success);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
@@ -630,7 +630,7 @@
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0, 184, 122, 0.1);
|
||||
border: 1px solid #00B87A;
|
||||
border: 1px solid var(--color-success);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
margin-bottom: var(--space-2);
|
||||
@@ -646,7 +646,7 @@
|
||||
|
||||
/* Specific styling for the early access warning container in import modal */
|
||||
.early-access-warning .warning-icon {
|
||||
color: #00B87A;
|
||||
color: var(--color-success);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
|
||||
@@ -114,8 +114,7 @@
|
||||
.sidebar-hidden-indicator {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
top: 68px; /* Align with sidebar header */
|
||||
z-index: var(--z-overlay);
|
||||
width: 14px;
|
||||
height: 44px;
|
||||
@@ -144,6 +143,21 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Subtle breathing animation for first-time discovery */
|
||||
@keyframes sidebarBreathing {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.65; }
|
||||
}
|
||||
|
||||
.sidebar-hidden-indicator.breathing {
|
||||
animation: sidebarBreathing 2.5s ease-in-out infinite;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.sidebar-hidden-indicator.breathing:hover {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.sidebar-hidden-indicator-tooltip {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
transition: var(--transition-slow);
|
||||
/* Add glow effect */
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(24, 144, 255, 0.3),
|
||||
0 0 20px rgba(24, 144, 255, 0.2),
|
||||
0 0 0 2px color-mix(in oklch, var(--color-accent) 30%, transparent),
|
||||
0 0 20px color-mix(in oklch, var(--color-accent) 20%, transparent),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@@ -221,14 +221,14 @@
|
||||
@keyframes onboarding-pulse {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(24, 144, 255, 0.4),
|
||||
0 0 20px rgba(24, 144, 255, 0.3),
|
||||
0 0 0 2px color-mix(in oklch, var(--color-accent) 40%, transparent),
|
||||
0 0 20px color-mix(in oklch, var(--color-accent) 30%, transparent),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(24, 144, 255, 0.6),
|
||||
0 0 30px rgba(24, 144, 255, 0.4),
|
||||
0 0 0 4px color-mix(in oklch, var(--color-accent) 60%, transparent),
|
||||
0 0 30px color-mix(in oklch, var(--color-accent) 40%, transparent),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@
|
||||
--color-error-border: color-mix(in oklch, var(--color-error) 50%, transparent);
|
||||
|
||||
--color-info: oklch(var(--color-info-l) var(--color-info-c) var(--color-info-h));
|
||||
--color-info-bg: oklch(72% 0.2 220);
|
||||
--color-info-text: oklch(28% 0.03 220);
|
||||
--color-info-glow: oklch(72% 0.2 220 / 0.28);
|
||||
--color-info-bg: oklch(var(--color-info-l) var(--color-info-c) var(--color-info-h));
|
||||
--color-info-text: oklch(28% 0.03 var(--color-info-h));
|
||||
--color-info-glow: oklch(var(--color-info-l) var(--color-info-c) var(--color-info-h) / 0.28);
|
||||
|
||||
--color-skip-refresh-bg: oklch(82% 0.12 45);
|
||||
--color-skip-refresh-text: oklch(35% 0.02 45);
|
||||
--color-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
|
||||
--color-skip-refresh-bg: oklch(82% 0.12 var(--color-warning-h));
|
||||
--color-skip-refresh-text: oklch(35% 0.02 var(--color-warning-h));
|
||||
--color-skip-refresh-glow: oklch(82% 0.12 var(--color-warning-h) / 0.15);
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -106,12 +106,360 @@
|
||||
--status-info-bg: oklch(50% 0.10 190 / 0.25);
|
||||
--status-info-border: oklch(55% 0.12 195 / 0.3);
|
||||
|
||||
--color-info-bg: oklch(62% 0.18 220);
|
||||
--color-info-text: oklch(98% 0.02 240);
|
||||
--color-info-glow: oklch(62% 0.18 220 / 0.4);
|
||||
--color-info-bg: oklch(62% 0.18 var(--color-info-h));
|
||||
--color-info-text: oklch(98% 0.02 var(--color-info-h));
|
||||
--color-info-glow: oklch(62% 0.18 var(--color-info-h) / 0.4);
|
||||
|
||||
--color-error-bg: color-mix(in oklch, var(--color-error) 15%, transparent);
|
||||
--color-error-border: color-mix(in oklch, var(--color-error) 40%, transparent);
|
||||
|
||||
--favorite-color: #ffc107;
|
||||
}
|
||||
|
||||
/* ── Preset: Nord ──────────────────────────────────────────── */
|
||||
|
||||
[data-theme-preset="nord"] {
|
||||
--color-accent-h: 213;
|
||||
--color-accent-c: 0.18;
|
||||
--color-accent-l: 62%;
|
||||
--color-warning-h: 35;
|
||||
--color-warning-c: 0.18;
|
||||
--color-success-h: 130;
|
||||
--color-error-l: 62%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 5;
|
||||
--color-info-h: 195;
|
||||
--color-info-c: 0.18;
|
||||
|
||||
--bg-base: oklch(96% 0.01 240);
|
||||
--bg-elevated: oklch(98% 0.008 240 / 0.95);
|
||||
--bg-hover: oklch(93% 0.02 240);
|
||||
--bg-disabled: oklch(92% 0.01 240);
|
||||
|
||||
--text-primary: oklch(22% 0.03 260);
|
||||
--text-secondary: oklch(48% 0.03 260);
|
||||
--text-inverse: oklch(97% 0.01 240);
|
||||
|
||||
--surface-base: oklch(97% 0.01 240);
|
||||
--surface-elevated: oklch(98% 0.008 240 / 0.95);
|
||||
--surface-hover: oklch(93% 0.02 240);
|
||||
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(82% 0.03 240);
|
||||
--border-subtle: oklch(82% 0.03 240 / 0.45);
|
||||
|
||||
--favorite-color: oklch(72% 0.14 85);
|
||||
--favorite-glow: oklch(72% 0.14 85 / 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"][data-theme-preset="nord"] {
|
||||
--color-accent-h: 213;
|
||||
--color-accent-c: 0.18;
|
||||
--color-accent-l: 68%;
|
||||
--color-warning-h: 35;
|
||||
--color-warning-c: 0.18;
|
||||
--color-success-h: 130;
|
||||
--color-error-l: 65%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 5;
|
||||
--color-info-h: 195;
|
||||
--color-info-c: 0.18;
|
||||
|
||||
--bg-base: oklch(20% 0.03 260);
|
||||
--bg-elevated: oklch(24% 0.03 260 / 0.98);
|
||||
--bg-hover: oklch(30% 0.03 260);
|
||||
--bg-disabled: oklch(30% 0.02 260);
|
||||
|
||||
--text-primary: oklch(87% 0.02 240);
|
||||
--text-secondary: oklch(68% 0.02 240);
|
||||
--text-inverse: oklch(20% 0.03 260);
|
||||
|
||||
--surface-base: oklch(26% 0.03 260);
|
||||
--surface-elevated: oklch(24% 0.03 260 / 0.98);
|
||||
--surface-hover: oklch(30% 0.03 260);
|
||||
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(38% 0.03 260);
|
||||
--border-subtle: oklch(87% 0.02 240 / 0.15);
|
||||
|
||||
--favorite-color: oklch(78% 0.15 85);
|
||||
--favorite-glow: oklch(78% 0.15 85 / 0.5);
|
||||
}
|
||||
|
||||
/* ── Preset: Midnight ───────────────────────────────────────── */
|
||||
|
||||
[data-theme-preset="midnight"] {
|
||||
--color-accent-h: 300;
|
||||
--color-accent-c: 0.15;
|
||||
--color-accent-l: 52%;
|
||||
--color-warning-h: 50;
|
||||
--color-warning-c: 0.18;
|
||||
--color-success-h: 135;
|
||||
--color-error-h: 5;
|
||||
--color-error-l: 62%;
|
||||
--color-error-c: 0.22;
|
||||
--color-info-h: 195;
|
||||
--color-info-c: 0.12;
|
||||
|
||||
--bg-base: oklch(96% 0.01 255);
|
||||
--bg-elevated: oklch(98% 0.008 255 / 0.95);
|
||||
--bg-hover: oklch(93% 0.02 255);
|
||||
--bg-disabled: oklch(92% 0.01 255);
|
||||
|
||||
--text-primary: oklch(22% 0.03 260);
|
||||
--text-secondary: oklch(48% 0.03 260);
|
||||
--text-inverse: oklch(97% 0.01 255);
|
||||
|
||||
--surface-base: oklch(97% 0.01 255);
|
||||
--surface-elevated: oklch(98% 0.008 255 / 0.95);
|
||||
--surface-hover: oklch(93% 0.02 255);
|
||||
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(80% 0.03 255);
|
||||
--border-subtle: oklch(80% 0.03 255 / 0.45);
|
||||
|
||||
--favorite-color: oklch(72% 0.16 85);
|
||||
--favorite-glow: oklch(72% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"][data-theme-preset="midnight"] {
|
||||
--color-accent-h: 300;
|
||||
--color-accent-c: 0.14;
|
||||
--color-accent-l: 68%;
|
||||
--color-warning-h: 50;
|
||||
--color-warning-c: 0.18;
|
||||
--color-success-h: 135;
|
||||
--color-error-h: 5;
|
||||
--color-error-l: 65%;
|
||||
--color-error-c: 0.22;
|
||||
--color-info-h: 195;
|
||||
--color-info-c: 0.12;
|
||||
|
||||
--bg-base: oklch(18% 0.03 260);
|
||||
--bg-elevated: oklch(22% 0.03 260 / 0.98);
|
||||
--bg-hover: oklch(28% 0.03 260);
|
||||
--bg-disabled: oklch(28% 0.02 260);
|
||||
|
||||
--text-primary: oklch(88% 0.02 255);
|
||||
--text-secondary: oklch(68% 0.02 255);
|
||||
--text-inverse: oklch(18% 0.03 260);
|
||||
|
||||
--surface-base: oklch(24% 0.03 260);
|
||||
--surface-elevated: oklch(22% 0.03 260 / 0.98);
|
||||
--surface-hover: oklch(28% 0.03 260);
|
||||
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(36% 0.03 260);
|
||||
--border-subtle: oklch(88% 0.02 255 / 0.15);
|
||||
|
||||
--favorite-color: oklch(78% 0.16 85);
|
||||
--favorite-glow: oklch(78% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
/* ── Preset: Monokai ───────────────────────────────────────── */
|
||||
|
||||
[data-theme-preset="monokai"] {
|
||||
--color-accent-h: 190;
|
||||
--color-accent-c: 0.24;
|
||||
--color-accent-l: 72%;
|
||||
--color-warning-h: 50;
|
||||
--color-warning-c: 0.22;
|
||||
--color-success-h: 140;
|
||||
--color-error-l: 60%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 340;
|
||||
--color-info-h: 250;
|
||||
|
||||
--bg-base: oklch(96% 0.01 80);
|
||||
--bg-elevated: oklch(98% 0.005 80 / 0.95);
|
||||
--bg-hover: oklch(93% 0.015 80);
|
||||
--bg-disabled: oklch(92% 0.01 80);
|
||||
|
||||
--text-primary: oklch(20% 0.02 100);
|
||||
--text-secondary: oklch(45% 0.02 100);
|
||||
--text-inverse: oklch(97% 0.01 80);
|
||||
|
||||
--surface-base: oklch(97% 0.008 80);
|
||||
--surface-elevated: oklch(98% 0.005 80 / 0.95);
|
||||
--surface-hover: oklch(93% 0.015 80);
|
||||
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(80% 0.02 80);
|
||||
--border-subtle: oklch(80% 0.02 80 / 0.45);
|
||||
|
||||
--favorite-color: oklch(72% 0.16 85);
|
||||
--favorite-glow: oklch(72% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"][data-theme-preset="monokai"] {
|
||||
--color-accent-h: 190;
|
||||
--color-accent-c: 0.24;
|
||||
--color-accent-l: 72%;
|
||||
--color-warning-h: 50;
|
||||
--color-warning-c: 0.22;
|
||||
--color-success-h: 140;
|
||||
--color-error-l: 65%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 340;
|
||||
--color-info-h: 250;
|
||||
|
||||
--bg-base: oklch(18% 0.02 100);
|
||||
--bg-elevated: oklch(22% 0.02 100 / 0.98);
|
||||
--bg-hover: oklch(28% 0.025 100);
|
||||
--bg-disabled: oklch(28% 0.015 100);
|
||||
|
||||
--text-primary: oklch(90% 0.02 80);
|
||||
--text-secondary: oklch(70% 0.02 80);
|
||||
--text-inverse: oklch(18% 0.02 100);
|
||||
|
||||
--surface-base: oklch(24% 0.02 100);
|
||||
--surface-elevated: oklch(22% 0.02 100 / 0.98);
|
||||
--surface-hover: oklch(28% 0.025 100);
|
||||
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(36% 0.02 100);
|
||||
--border-subtle: oklch(90% 0.02 80 / 0.15);
|
||||
|
||||
--favorite-color: oklch(78% 0.16 85);
|
||||
--favorite-glow: oklch(78% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
/* ── Preset: Dracula ───────────────────────────────────────── */
|
||||
|
||||
[data-theme-preset="dracula"] {
|
||||
--color-accent-h: 265;
|
||||
--color-accent-c: 0.24;
|
||||
--color-accent-l: 68%;
|
||||
--color-warning-h: 45;
|
||||
--color-warning-c: 0.22;
|
||||
--color-success-h: 135;
|
||||
--color-error-l: 62%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 350;
|
||||
--color-info-h: 195;
|
||||
|
||||
--bg-base: oklch(96% 0.01 290);
|
||||
--bg-elevated: oklch(98% 0.008 290 / 0.95);
|
||||
--bg-hover: oklch(93% 0.02 290);
|
||||
--bg-disabled: oklch(92% 0.01 290);
|
||||
|
||||
--text-primary: oklch(22% 0.04 290);
|
||||
--text-secondary: oklch(48% 0.04 290);
|
||||
--text-inverse: oklch(97% 0.01 290);
|
||||
|
||||
--surface-base: oklch(97% 0.01 290);
|
||||
--surface-elevated: oklch(98% 0.008 290 / 0.95);
|
||||
--surface-hover: oklch(93% 0.02 290);
|
||||
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(80% 0.04 290);
|
||||
--border-subtle: oklch(80% 0.04 290 / 0.45);
|
||||
|
||||
--favorite-color: oklch(72% 0.16 85);
|
||||
--favorite-glow: oklch(72% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"][data-theme-preset="dracula"] {
|
||||
--color-accent-h: 265;
|
||||
--color-accent-c: 0.24;
|
||||
--color-accent-l: 72%;
|
||||
--color-warning-h: 45;
|
||||
--color-warning-c: 0.22;
|
||||
--color-success-h: 135;
|
||||
--color-error-l: 65%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 350;
|
||||
--color-info-h: 195;
|
||||
|
||||
--bg-base: oklch(18% 0.04 290);
|
||||
--bg-elevated: oklch(22% 0.04 290 / 0.98);
|
||||
--bg-hover: oklch(28% 0.04 290);
|
||||
--bg-disabled: oklch(28% 0.03 290);
|
||||
|
||||
--text-primary: oklch(90% 0.02 290);
|
||||
--text-secondary: oklch(70% 0.03 290);
|
||||
--text-inverse: oklch(18% 0.04 290);
|
||||
|
||||
--surface-base: oklch(24% 0.04 290);
|
||||
--surface-elevated: oklch(22% 0.04 290 / 0.98);
|
||||
--surface-hover: oklch(28% 0.04 290);
|
||||
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(36% 0.04 290);
|
||||
--border-subtle: oklch(90% 0.02 290 / 0.15);
|
||||
|
||||
--favorite-color: oklch(78% 0.16 85);
|
||||
--favorite-glow: oklch(78% 0.16 85 / 0.5);
|
||||
}
|
||||
|
||||
/* ── Preset: Solarized ─────────────────────────────────────── */
|
||||
|
||||
[data-theme-preset="solarized"] {
|
||||
--color-accent-h: 175;
|
||||
--color-accent-c: 0.18;
|
||||
--color-accent-l: 55%;
|
||||
--color-warning-h: 45;
|
||||
--color-warning-c: 0.20;
|
||||
--color-success-h: 68;
|
||||
--color-error-l: 62%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 5;
|
||||
--color-info-h: 220;
|
||||
--color-info-c: 0.16;
|
||||
--color-info-l: 68%;
|
||||
|
||||
--bg-base: oklch(95% 0.03 85);
|
||||
--bg-elevated: oklch(97% 0.025 85 / 0.95);
|
||||
--bg-hover: oklch(91% 0.035 85);
|
||||
--bg-disabled: oklch(90% 0.025 85);
|
||||
|
||||
--text-primary: oklch(30% 0.06 200);
|
||||
--text-secondary: oklch(50% 0.04 200);
|
||||
--text-inverse: oklch(95% 0.03 85);
|
||||
|
||||
--surface-base: oklch(96% 0.025 85);
|
||||
--surface-elevated: oklch(97% 0.025 85 / 0.95);
|
||||
--surface-hover: oklch(91% 0.035 85);
|
||||
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(78% 0.04 85);
|
||||
--border-subtle: oklch(78% 0.04 85 / 0.45);
|
||||
|
||||
--favorite-color: oklch(68% 0.16 75);
|
||||
--favorite-glow: oklch(68% 0.16 75 / 0.5);
|
||||
}
|
||||
|
||||
[data-theme="dark"][data-theme-preset="solarized"] {
|
||||
--color-accent-h: 175;
|
||||
--color-accent-c: 0.18;
|
||||
--color-accent-l: 60%;
|
||||
--color-warning-h: 45;
|
||||
--color-warning-c: 0.20;
|
||||
--color-success-h: 68;
|
||||
--color-error-l: 65%;
|
||||
--color-error-c: 0.22;
|
||||
--color-error-h: 5;
|
||||
--color-info-h: 220;
|
||||
--color-info-c: 0.16;
|
||||
--color-info-l: 68%;
|
||||
|
||||
--bg-base: oklch(18% 0.05 200);
|
||||
--bg-elevated: oklch(22% 0.05 200 / 0.98);
|
||||
--bg-hover: oklch(28% 0.05 200);
|
||||
--bg-disabled: oklch(28% 0.04 200);
|
||||
|
||||
--text-primary: oklch(72% 0.03 85);
|
||||
--text-secondary: oklch(62% 0.03 85);
|
||||
--text-inverse: oklch(18% 0.05 200);
|
||||
|
||||
--surface-base: oklch(24% 0.05 200);
|
||||
--surface-elevated: oklch(22% 0.05 200 / 0.98);
|
||||
--surface-hover: oklch(28% 0.05 200);
|
||||
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||
|
||||
--border-base: oklch(36% 0.04 200);
|
||||
--border-subtle: oklch(72% 0.03 85 / 0.15);
|
||||
|
||||
--favorite-color: oklch(72% 0.16 75);
|
||||
--favorite-glow: oklch(72% 0.16 75 / 0.5);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { updateService } from '../managers/UpdateService.js';
|
||||
import { toggleTheme } from '../utils/uiHelpers.js';
|
||||
import { toggleTheme, setPreset, CYCLE_ORDER, PRESET_NAMES } from '../utils/uiHelpers.js';
|
||||
import { SearchManager } from '../managers/SearchManager.js';
|
||||
import { FilterManager } from '../managers/FilterManager.js';
|
||||
import { initPageState } from '../state/index.js';
|
||||
import { getStorageItem } from '../utils/storageHelpers.js';
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { updateElementAttribute } from '../utils/i18nHelpers.js';
|
||||
import { renderSupporters } from '../services/supportersService.js';
|
||||
|
||||
@@ -47,25 +47,8 @@ export class HeaderManager {
|
||||
}
|
||||
|
||||
initializeCommonElements() {
|
||||
// Handle theme toggle
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
if (themeToggle) {
|
||||
const currentTheme = getStorageItem('theme') || 'auto';
|
||||
themeToggle.classList.add(`theme-${currentTheme}`);
|
||||
this.initializeThemePopover();
|
||||
|
||||
// Use i18nHelpers to update themeToggle's title
|
||||
this.updateThemeTooltip(themeToggle, currentTheme);
|
||||
|
||||
themeToggle.addEventListener('click', async () => {
|
||||
if (typeof toggleTheme === 'function') {
|
||||
const newTheme = toggleTheme();
|
||||
// Use i18nHelpers to update themeToggle's title
|
||||
this.updateThemeTooltip(themeToggle, newTheme);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle settings toggle
|
||||
const settingsToggle = document.querySelector('.settings-toggle');
|
||||
if (settingsToggle) {
|
||||
settingsToggle.addEventListener('click', () => {
|
||||
@@ -74,22 +57,19 @@ export class HeaderManager {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle update toggle
|
||||
|
||||
const updateToggle = document.getElementById('updateToggleBtn');
|
||||
if (updateToggle) {
|
||||
updateToggle.addEventListener('click', () => {
|
||||
updateService.toggleUpdateModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Handle support toggle
|
||||
|
||||
const supportToggle = document.getElementById('supportToggleBtn');
|
||||
if (supportToggle) {
|
||||
supportToggle.addEventListener('click', async () => {
|
||||
if (window.modalManager) {
|
||||
window.modalManager.toggleModal('supportModal');
|
||||
// Load supporters data when modal opens
|
||||
try {
|
||||
await renderSupporters();
|
||||
} catch (error) {
|
||||
@@ -99,41 +79,126 @@ export class HeaderManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Handle QR code toggle
|
||||
const qrToggle = document.getElementById('toggleQRCode');
|
||||
const qrContainer = document.getElementById('qrCodeContainer');
|
||||
|
||||
if (qrToggle && qrContainer) {
|
||||
qrToggle.addEventListener('click', function() {
|
||||
qrContainer.classList.toggle('show');
|
||||
qrToggle.classList.toggle('active');
|
||||
|
||||
const toggleText = qrToggle.querySelector('.toggle-text');
|
||||
if (qrContainer.classList.contains('show')) {
|
||||
toggleText.textContent = 'Hide WeChat QR Code';
|
||||
// Add small delay to ensure DOM is updated before scrolling
|
||||
setTimeout(() => {
|
||||
const supportModal = document.querySelector('.support-modal');
|
||||
if (supportModal) {
|
||||
supportModal.scrollTo({
|
||||
top: supportModal.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 250);
|
||||
} else {
|
||||
toggleText.textContent = 'Show WeChat QR Code';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hide search functionality on Statistics page
|
||||
this.updateHeaderForPage();
|
||||
|
||||
// Initialize hamburger menu for mobile
|
||||
if (qrToggle && qrContainer) {
|
||||
qrToggle.addEventListener('click', function () {
|
||||
qrContainer.classList.toggle('show');
|
||||
qrToggle.classList.toggle('active');
|
||||
|
||||
const toggleText = qrToggle.querySelector('.toggle-text');
|
||||
if (qrContainer.classList.contains('show')) {
|
||||
toggleText.textContent = 'Hide WeChat QR Code';
|
||||
setTimeout(() => {
|
||||
const supportModal = document.querySelector('.support-modal');
|
||||
if (supportModal) {
|
||||
supportModal.scrollTo({
|
||||
top: supportModal.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 250);
|
||||
} else {
|
||||
toggleText.textContent = 'Show WeChat QR Code';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.updateHeaderForPage();
|
||||
this.initializeHamburgerMenu();
|
||||
}
|
||||
|
||||
initializeThemePopover() {
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
const themePopover = document.getElementById('themePopover');
|
||||
if (!themeToggle || !themePopover) return;
|
||||
|
||||
const currentTheme = getStorageItem('theme') || 'auto';
|
||||
const currentPreset = getStorageItem('theme_preset') || 'default';
|
||||
themeToggle.classList.add(`theme-${currentTheme}`);
|
||||
this.updateThemeTooltip(themeToggle, currentTheme);
|
||||
this.updatePopoverActiveStates(currentTheme, currentPreset);
|
||||
|
||||
themeToggle.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.theme-popover')) return;
|
||||
e.stopPropagation();
|
||||
const isOpen = themePopover.classList.contains('active');
|
||||
this.closeAllPopovers();
|
||||
if (!isOpen) {
|
||||
themePopover.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
themePopover.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const modeBtn = e.target.closest('.theme-mode-btn');
|
||||
const presetBtn = e.target.closest('.theme-preset-btn');
|
||||
|
||||
if (modeBtn) {
|
||||
const mode = modeBtn.dataset.mode;
|
||||
this.setThemeMode(mode);
|
||||
} else if (presetBtn) {
|
||||
const preset = presetBtn.dataset.preset;
|
||||
this.setThemePreset(preset);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!themeToggle.contains(e.target) && !themePopover.contains(e.target)) {
|
||||
themePopover.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeAllPopovers() {
|
||||
const themePopover = document.getElementById('themePopover');
|
||||
if (themePopover) {
|
||||
themePopover.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
setThemeMode(mode) {
|
||||
setStorageItem('theme', mode);
|
||||
const htmlElement = document.documentElement;
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
htmlElement.removeAttribute('data-theme');
|
||||
if (mode === 'dark' || (mode === 'auto' && prefersDark)) {
|
||||
htmlElement.setAttribute('data-theme', 'dark');
|
||||
document.body.dataset.theme = 'dark';
|
||||
} else {
|
||||
htmlElement.setAttribute('data-theme', 'light');
|
||||
document.body.dataset.theme = 'light';
|
||||
}
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
|
||||
themeToggle.classList.add(`theme-${mode}`);
|
||||
this.updateThemeTooltip(themeToggle, mode);
|
||||
}
|
||||
this.updateHamburgerThemeIcon();
|
||||
this.updatePopoverActiveStates(mode, getStorageItem('theme_preset') || 'default');
|
||||
}
|
||||
|
||||
setThemePreset(preset) {
|
||||
setPreset(preset);
|
||||
this.updatePopoverActiveStates(getStorageItem('theme') || 'auto', preset);
|
||||
this.updateHamburgerThemeIcon();
|
||||
}
|
||||
|
||||
updatePopoverActiveStates(theme, preset) {
|
||||
const popover = document.getElementById('themePopover');
|
||||
if (!popover) return;
|
||||
|
||||
popover.querySelectorAll('.theme-mode-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.mode === theme);
|
||||
});
|
||||
|
||||
popover.querySelectorAll('.theme-preset-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.preset === preset);
|
||||
});
|
||||
}
|
||||
|
||||
initializeHamburgerMenu() {
|
||||
const hamburgerBtn = document.getElementById('hamburgerMenuBtn');
|
||||
const hamburgerDropdown = document.getElementById('hamburgerDropdown');
|
||||
@@ -188,7 +253,6 @@ export class HeaderManager {
|
||||
case 'theme':
|
||||
if (typeof toggleTheme === 'function') {
|
||||
const newTheme = toggleTheme();
|
||||
// Update theme toggle in header if it exists
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
|
||||
@@ -196,6 +260,7 @@ export class HeaderManager {
|
||||
this.updateThemeTooltip(themeToggle, newTheme);
|
||||
}
|
||||
this.updateHamburgerThemeIcon();
|
||||
this.updatePopoverActiveStates(newTheme, getStorageItem('theme_preset') || 'default');
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
|
||||
@@ -1040,7 +1040,15 @@ export class SidebarManager {
|
||||
<span class="sidebar-hidden-indicator-tooltip">${translate('sidebar.showSidebar')}</span>
|
||||
`;
|
||||
|
||||
// Subtle breathing animation on first sight to aid discoverability;
|
||||
// stops permanently after user clicks the restore button once
|
||||
const restoreKey = `${this.pageType}_restoreButtonUsed`;
|
||||
if (!getStorageItem(restoreKey, false)) {
|
||||
indicator.classList.add('breathing');
|
||||
}
|
||||
|
||||
indicator.addEventListener('click', () => {
|
||||
setStorageItem(restoreKey, true);
|
||||
this.toggleHideOnThisPage();
|
||||
});
|
||||
|
||||
|
||||
@@ -240,6 +240,9 @@ export class StatisticsManager {
|
||||
|
||||
// Storage efficiency chart
|
||||
this.createStorageEfficiencyChart();
|
||||
|
||||
// Model types chart (Collection tab)
|
||||
this.createModelTypesChart();
|
||||
}
|
||||
|
||||
createCollectionPieChart() {
|
||||
@@ -554,6 +557,68 @@ export class StatisticsManager {
|
||||
});
|
||||
}
|
||||
|
||||
createModelTypesChart() {
|
||||
const ctx = document.getElementById('modelTypesChart');
|
||||
if (!ctx || !this.data.collection || !this.data.collection.model_types_distribution) return;
|
||||
|
||||
const distribution = this.data.collection.model_types_distribution;
|
||||
const typeDisplayNames = {
|
||||
lora: 'LoRA',
|
||||
locon: 'LyCORIS',
|
||||
dora: 'DoRA',
|
||||
checkpoint: 'Checkpoint',
|
||||
diffusion_model: 'Diffusion Model',
|
||||
embedding: 'Embeddings'
|
||||
};
|
||||
|
||||
const colorPalette = {
|
||||
lora: 'oklch(68% 0.28 256)',
|
||||
locon: 'oklch(68% 0.25 190)',
|
||||
dora: 'oklch(68% 0.25 330)',
|
||||
checkpoint: 'oklch(68% 0.28 45)',
|
||||
diffusion_model: 'oklch(68% 0.25 280)',
|
||||
embedding: 'oklch(68% 0.25 120)'
|
||||
};
|
||||
|
||||
const labels = Object.keys(distribution).map(k => typeDisplayNames[k] || k);
|
||||
const values = Object.values(distribution);
|
||||
const colors = Object.keys(distribution).map(k => colorPalette[k] || 'oklch(68% 0.15 0)');
|
||||
|
||||
const data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: values,
|
||||
backgroundColor: colors,
|
||||
borderColor: getComputedStyle(document.documentElement).getPropertyValue('--border-color'),
|
||||
borderWidth: 2
|
||||
}]
|
||||
};
|
||||
|
||||
this.charts.modelTypes = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const value = context.parsed;
|
||||
const pct = ((value / total) * 100).toFixed(1);
|
||||
return ` ${context.label}: ${value} (${pct}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLists() {
|
||||
const listTypes = [
|
||||
{ type: 'lora', containerId: 'topLorasList' },
|
||||
|
||||
@@ -197,11 +197,22 @@ export function restoreFolderFilter() {
|
||||
}
|
||||
}
|
||||
|
||||
const CYCLE_ORDER = ['auto', 'light', 'dark'];
|
||||
const PRESET_NAMES = ['default', 'nord', 'midnight', 'monokai', 'dracula', 'solarized'];
|
||||
|
||||
export { CYCLE_ORDER, PRESET_NAMES };
|
||||
|
||||
export function initTheme() {
|
||||
const savedTheme = getStorageItem('theme') || 'auto';
|
||||
// Migrate deprecated presets
|
||||
let savedPreset = getStorageItem('theme_preset');
|
||||
if (savedPreset === 'gruvbox') {
|
||||
savedPreset = 'midnight';
|
||||
setStorageItem('theme_preset', 'midnight');
|
||||
}
|
||||
applyTheme(savedTheme);
|
||||
applyPreset(savedPreset || 'default');
|
||||
|
||||
// Update theme when system preference changes (for 'auto' mode)
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
const currentTheme = getStorageItem('theme') || 'auto';
|
||||
if (currentTheme === 'auto') {
|
||||
@@ -212,34 +223,44 @@ export function initTheme() {
|
||||
|
||||
export function toggleTheme() {
|
||||
const currentTheme = getStorageItem('theme') || 'auto';
|
||||
let newTheme;
|
||||
|
||||
if (currentTheme === 'light') {
|
||||
newTheme = 'dark';
|
||||
} else {
|
||||
newTheme = 'light';
|
||||
}
|
||||
const currentIndex = CYCLE_ORDER.indexOf(currentTheme);
|
||||
const nextIndex = (currentIndex + 1) % CYCLE_ORDER.length;
|
||||
const newTheme = CYCLE_ORDER[nextIndex];
|
||||
|
||||
setStorageItem('theme', newTheme);
|
||||
applyTheme(newTheme);
|
||||
|
||||
// Force a repaint to ensure theme changes are applied immediately
|
||||
document.body.style.display = 'none';
|
||||
document.body.offsetHeight; // Trigger a reflow
|
||||
document.body.offsetHeight;
|
||||
document.body.style.display = '';
|
||||
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
// Add a new helper function to apply the theme
|
||||
export function cyclePreset() {
|
||||
const currentPreset = getStorageItem('theme_preset') || 'default';
|
||||
const currentIndex = PRESET_NAMES.indexOf(currentPreset);
|
||||
const nextIndex = (currentIndex + 1) % PRESET_NAMES.length;
|
||||
const newPreset = PRESET_NAMES[nextIndex];
|
||||
|
||||
setStorageItem('theme_preset', newPreset);
|
||||
applyPreset(newPreset);
|
||||
|
||||
return newPreset;
|
||||
}
|
||||
|
||||
export function setPreset(name) {
|
||||
if (!PRESET_NAMES.includes(name)) return;
|
||||
setStorageItem('theme_preset', name);
|
||||
applyPreset(name);
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
// Remove any existing theme attributes
|
||||
htmlElement.removeAttribute('data-theme');
|
||||
|
||||
// Apply the appropriate theme
|
||||
if (theme === 'dark' || (theme === 'auto' && prefersDark)) {
|
||||
htmlElement.setAttribute('data-theme', 'dark');
|
||||
document.body.dataset.theme = 'dark';
|
||||
@@ -248,19 +269,18 @@ function applyTheme(theme) {
|
||||
document.body.dataset.theme = 'light';
|
||||
}
|
||||
|
||||
// Update the theme-toggle icon state
|
||||
updateThemeToggleIcons(theme);
|
||||
}
|
||||
|
||||
// New function to update theme toggle icons
|
||||
function applyPreset(preset) {
|
||||
document.documentElement.setAttribute('data-theme-preset', preset);
|
||||
}
|
||||
|
||||
function updateThemeToggleIcons(theme) {
|
||||
const themeToggle = document.querySelector('.theme-toggle');
|
||||
if (!themeToggle) return;
|
||||
|
||||
// Remove any existing active classes
|
||||
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
|
||||
|
||||
// Add the appropriate class based on current theme
|
||||
themeToggle.classList.add(`theme-${theme}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,16 +46,20 @@
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
// Apply theme immediately based on stored preference
|
||||
const STORAGE_PREFIX = 'lora_manager_';
|
||||
const savedTheme = localStorage.getItem(STORAGE_PREFIX + 'theme') || 'auto';
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var STORAGE_PREFIX = 'lora_manager_';
|
||||
var savedTheme = localStorage.getItem(STORAGE_PREFIX + 'theme') || 'auto';
|
||||
var savedPreset = localStorage.getItem(STORAGE_PREFIX + 'theme_preset') || 'default';
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (savedTheme === 'dark' || (savedTheme === 'auto' && prefersDark)) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
}
|
||||
|
||||
if (savedPreset && savedPreset !== 'default') {
|
||||
document.documentElement.setAttribute('data-theme-preset', savedPreset);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% block head_scripts %}{% endblock %}
|
||||
|
||||
@@ -72,6 +72,55 @@
|
||||
<i class="fas fa-moon dark-icon"></i>
|
||||
<i class="fas fa-sun light-icon"></i>
|
||||
<i class="fas fa-adjust auto-icon"></i>
|
||||
<div class="theme-popover" id="themePopover">
|
||||
<div class="theme-popover-section">
|
||||
<div class="theme-popover-label">{{ t('header.theme.mode') }}</div>
|
||||
<div class="theme-popover-modes">
|
||||
<button class="theme-mode-btn" data-mode="light" title="{{ t('header.theme.light') }}">
|
||||
<i class="fas fa-sun"></i>
|
||||
<span>{{ t('header.theme.light') }}</span>
|
||||
</button>
|
||||
<button class="theme-mode-btn" data-mode="dark" title="{{ t('header.theme.dark') }}">
|
||||
<i class="fas fa-moon"></i>
|
||||
<span>{{ t('header.theme.dark') }}</span>
|
||||
</button>
|
||||
<button class="theme-mode-btn" data-mode="auto" title="{{ t('header.theme.auto') }}">
|
||||
<i class="fas fa-adjust"></i>
|
||||
<span>{{ t('header.theme.auto') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-popover-divider"></div>
|
||||
<div class="theme-popover-section">
|
||||
<div class="theme-popover-label">{{ t('header.theme.presets') }}</div>
|
||||
<div class="theme-popover-presets">
|
||||
<button class="theme-preset-btn" data-preset="default" title="{{ t('header.theme.default') }}">
|
||||
<span class="preset-swatch preset-swatch-default"></span>
|
||||
<span>{{ t('header.theme.default') }}</span>
|
||||
</button>
|
||||
<button class="theme-preset-btn" data-preset="nord" title="{{ t('header.theme.nord') }}">
|
||||
<span class="preset-swatch preset-swatch-nord"></span>
|
||||
<span>{{ t('header.theme.nord') }}</span>
|
||||
</button>
|
||||
<button class="theme-preset-btn" data-preset="midnight" title="{{ t('header.theme.midnight') }}">
|
||||
<span class="preset-swatch preset-swatch-midnight"></span>
|
||||
<span>{{ t('header.theme.midnight') }}</span>
|
||||
</button>
|
||||
<button class="theme-preset-btn" data-preset="monokai" title="{{ t('header.theme.monokai') }}">
|
||||
<span class="preset-swatch preset-swatch-monokai"></span>
|
||||
<span>{{ t('header.theme.monokai') }}</span>
|
||||
</button>
|
||||
<button class="theme-preset-btn" data-preset="dracula" title="{{ t('header.theme.dracula') }}">
|
||||
<span class="preset-swatch preset-swatch-dracula"></span>
|
||||
<span>{{ t('header.theme.dracula') }}</span>
|
||||
</button>
|
||||
<button class="theme-preset-btn" data-preset="solarized" title="{{ t('header.theme.solarized') }}">
|
||||
<span class="preset-swatch preset-swatch-solarized"></span>
|
||||
<span>{{ t('header.theme.solarized') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-toggle" title="{{ t('common.actions.settings') }}">
|
||||
<i class="fas fa-cog"></i>
|
||||
|
||||
Reference in New Issue
Block a user