diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 22cf2757..9ffa7d58 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -488,12 +488,22 @@ class ApiRoutes: }) async def get_civitai_versions(self, request: web.Request) -> web.Response: - """Get available versions for a Civitai model""" + """Get available versions for a Civitai model with local availability info""" try: model_id = request.match_info['model_id'] versions = await self.civitai_client.get_model_versions(model_id) if not versions: return web.Response(status=404, text="Model not found") + + # Check local availability for each version + for version in versions: + for file in version.get('files', []): + sha256 = file.get('hashes', {}).get('SHA256') + if sha256: + file['existsLocally'] = self.scanner.has_lora_hash(sha256) + if file['existsLocally']: + file['localPath'] = self.scanner.get_lora_path_by_hash(sha256) + return web.json_response(versions) except Exception as e: logger.error(f"Error fetching model versions: {e}") diff --git a/py/services/civitai_client.py b/py/services/civitai_client.py index fe002ed9..ae08e56c 100644 --- a/py/services/civitai_client.py +++ b/py/services/civitai_client.py @@ -4,7 +4,7 @@ import os import json import logging from email.parser import Parser -from typing import Optional, Dict, Tuple +from typing import Optional, Dict, Tuple, List from urllib.parse import unquote from ..utils.models import LoraMetadata @@ -135,16 +135,15 @@ class CivitaiClient: print(f"Download Error: {str(e)}") return False - async def get_model_versions(self, model_id: str) -> Optional[Dict]: - """Fetch all versions of a model""" + async def get_model_versions(self, model_id: str) -> List[Dict]: + """Get all versions of a model with local availability info""" try: - session = await self.session - url = f"{self.base_url}/models/{model_id}" - async with session.get(url, headers=self.headers) as response: - if response.status == 200: - data = await response.json() - return data.get('modelVersions', []) - return None + session = await self.session # 等待获取 session + async with session.get(f"{self.base_url}/models/{model_id}") as response: + if response.status != 200: + return None + data = await response.json() + return data.get('modelVersions', []) except Exception as e: logger.error(f"Error fetching model versions: {e}") return None diff --git a/py/services/file_monitor.py b/py/services/file_monitor.py index 00d30a20..3bef2dd5 100644 --- a/py/services/file_monitor.py +++ b/py/services/file_monitor.py @@ -86,26 +86,32 @@ class LoraFileHandler(FileSystemEventHandler): if not changes: return - logger.info(f"Processing {len(changes)} file changes") - cache = await self.scanner.get_cached_data() # 先完成可能的初始化 + cache = await self.scanner.get_cached_data() needs_resort = False - new_folders = set() # 用于收集新的文件夹 + new_folders = set() for action, file_path in changes: try: if action == 'add': - # 扫描新文件 + # Scan new file lora_data = await self.scanner.scan_single_lora(file_path) if lora_data: cache.raw_data.append(lora_data) - new_folders.add(lora_data['folder']) # 收集新文件夹 + new_folders.add(lora_data['folder']) + # Update hash index + if 'sha256' in lora_data: + self.scanner._hash_index.add_entry( + lora_data['sha256'], + lora_data['file_path'] + ) needs_resort = True elif action == 'remove': - # 从缓存中移除 + # Remove from cache and hash index logger.info(f"Removing {file_path} from cache") + self.scanner._hash_index.remove_by_path(file_path) cache.raw_data = [ item for item in cache.raw_data if item['file_path'] != file_path @@ -118,7 +124,7 @@ class LoraFileHandler(FileSystemEventHandler): if needs_resort: await cache.resort() - # 更新文件夹列表,包括新添加的文件夹 + # Update folder list all_folders = set(cache.folders) | new_folders cache.folders = sorted(list(all_folders), key=lambda x: x.lower()) diff --git a/py/services/lora_hash_index.py b/py/services/lora_hash_index.py new file mode 100644 index 00000000..f5b6c4e7 --- /dev/null +++ b/py/services/lora_hash_index.py @@ -0,0 +1,48 @@ +from typing import Dict, Optional +import logging +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + +@dataclass +class LoraHashIndex: + """Index for mapping LoRA file hashes to their file paths""" + + def __init__(self): + self._hash_to_path: Dict[str, str] = {} + + def add_entry(self, sha256: str, file_path: str) -> None: + """Add or update a hash -> path mapping""" + if not sha256 or not file_path: + return + self._hash_to_path[sha256] = file_path + + def remove_entry(self, sha256: str) -> None: + """Remove a hash entry""" + self._hash_to_path.pop(sha256, None) + + def remove_by_path(self, file_path: str) -> None: + """Remove entry by file path""" + for sha256, path in list(self._hash_to_path.items()): + if path == file_path: + del self._hash_to_path[sha256] + break + + def get_path(self, sha256: str) -> Optional[str]: + """Get file path for a given hash""" + return self._hash_to_path.get(sha256) + + def get_hash(self, file_path: str) -> Optional[str]: + """Get hash for a given file path""" + for sha256, path in self._hash_to_path.items(): + if path == file_path: + return sha256 + return None + + def has_hash(self, sha256: str) -> bool: + """Check if hash exists in index""" + return sha256 in self._hash_to_path + + def clear(self) -> None: + """Clear all entries""" + self._hash_to_path.clear() \ No newline at end of file diff --git a/py/services/lora_scanner.py b/py/services/lora_scanner.py index 5d3fbdf4..45e259fa 100644 --- a/py/services/lora_scanner.py +++ b/py/services/lora_scanner.py @@ -10,6 +10,7 @@ from ..config import config from ..utils.file_utils import load_metadata, get_file_info from .lora_cache import LoraCache from difflib import SequenceMatcher +from .lora_hash_index import LoraHashIndex logger = logging.getLogger(__name__) @@ -28,6 +29,7 @@ class LoraScanner: # 确保初始化只执行一次 if not hasattr(self, '_initialized'): self._cache: Optional[LoraCache] = None + self._hash_index = LoraHashIndex() self._initialization_lock = asyncio.Lock() self._initialization_task: Optional[asyncio.Task] = None self._initialized = True @@ -85,9 +87,17 @@ class LoraScanner: async def _initialize_cache(self) -> None: """Initialize or refresh the cache""" try: + # Clear existing hash index + self._hash_index.clear() + # Scan for new data raw_data = await self.scan_all_loras() + # Build hash index + for lora_data in raw_data: + if 'sha256' in lora_data and 'file_path' in lora_data: + self._hash_index.add_entry(lora_data['sha256'], lora_data['file_path']) + # Update cache self._cache = LoraCache( raw_data=raw_data, @@ -416,13 +426,23 @@ class LoraScanner: async def update_single_lora_cache(self, original_path: str, new_path: str, metadata: Dict) -> bool: cache = await self.get_cached_data() + + # Remove old path from hash index if exists + self._hash_index.remove_by_path(original_path) + cache.raw_data = [ - item for item in cache.raw_data - if item['file_path'] != original_path - ] + item for item in cache.raw_data + if item['file_path'] != original_path + ] + if metadata: metadata['folder'] = self._calculate_folder(new_path) cache.raw_data.append(metadata) + + # Update hash index with new path + if 'sha256' in metadata: + self._hash_index.add_entry(metadata['sha256'], new_path) + all_folders = set(cache.folders) all_folders.add(metadata['folder']) cache.folders = sorted(list(all_folders), key=lambda x: x.lower()) @@ -430,7 +450,6 @@ class LoraScanner: # Resort cache await cache.resort() - async def _update_metadata_paths(self, metadata_path: str, lora_path: str) -> Dict: """Update file paths in metadata file""" try: @@ -457,3 +476,16 @@ class LoraScanner: except Exception as e: logger.error(f"Error updating metadata paths: {e}", exc_info=True) + # Add new methods for hash index functionality + def has_lora_hash(self, sha256: str) -> bool: + """Check if a LoRA with given hash exists""" + return self._hash_index.has_hash(sha256) + + def get_lora_path_by_hash(self, sha256: str) -> Optional[str]: + """Get file path for a LoRA by its hash""" + return self._hash_index.get_path(sha256) + + def get_lora_hash_by_path(self, file_path: str) -> Optional[str]: + """Get hash for a LoRA by its file path""" + return self._hash_index.get_hash(file_path) + diff --git a/static/css/components/download-modal.css b/static/css/components/download-modal.css new file mode 100644 index 00000000..ae528b87 --- /dev/null +++ b/static/css/components/download-modal.css @@ -0,0 +1,240 @@ +/* Download Modal Styles */ +.download-step { + margin: var(--space-2) 0; +} + +.input-group { + margin-bottom: var(--space-2); +} + +.input-group label { + display: block; + margin-bottom: 8px; + color: var(--text-color); +} + +.input-group input, +.input-group select { + width: 100%; + padding: 8px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + background: var(--bg-color); + color: var(--text-color); +} + +.error-message { + color: var(--lora-error); + font-size: 0.9em; + margin-top: 4px; +} + +/* Version List Styles */ +.version-list { + max-height: 400px; + overflow-y: auto; + margin: var(--space-2) 0; + display: flex; + flex-direction: column; + gap: 12px; + padding: 1px; +} + +.version-item { + display: flex; + gap: var(--space-2); + padding: var(--space-2); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: all 0.2s ease; + background: var(--bg-color); + margin: 1px; + position: relative; +} + +.version-item:hover { + border-color: var(--lora-accent); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 1; +} + +.version-item.selected { + border: 2px solid var(--lora-accent); + background: oklch(var(--lora-accent) / 0.05); +} + +.version-thumbnail { + width: 80px; + height: 80px; + flex-shrink: 0; + border-radius: var(--border-radius-xs); + overflow: hidden; + background: var(--bg-color); +} + +.version-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.version-content { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 0; +} + +.version-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-2); +} + +.version-content h3 { + margin: 0; + font-size: 1.1em; + color: var(--text-color); + flex: 1; +} + +.version-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + font-size: 0.9em; +} + +.version-info .base-model { + background: oklch(var(--lora-accent) / 0.1); + color: var(--lora-accent); + padding: 2px 8px; + border-radius: var(--border-radius-xs); +} + +.version-meta { + display: flex; + gap: 12px; + font-size: 0.85em; + color: var(--text-color); + opacity: 0.7; +} + +.version-meta span { + display: flex; + align-items: center; + gap: 4px; +} + +/* Local Version Badge */ +.local-badge { + display: inline-flex; + align-items: center; + background: var(--lora-accent); + color: var(--lora-text); + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; + position: relative; +} + +.local-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +.local-path { + display: none; + position: absolute; + top: 100%; + right: 0; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + margin-top: 4px; + font-size: 0.9em; + color: var(--text-color); + white-space: normal; + word-break: break-all; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: 1; + min-width: 200px; + max-width: 300px; +} + +.local-badge:hover .local-path { + display: block; +} + +/* Folder Browser Styles */ +.folder-browser { + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + max-height: 200px; + overflow-y: auto; +} + +.folder-item { + padding: 8px; + cursor: pointer; + border-radius: var(--border-radius-xs); + transition: background-color 0.2s; +} + +.folder-item:hover { + background: var(--lora-surface); +} + +.folder-item.selected { + background: oklch(var(--lora-accent) / 0.1); + border: 1px solid var(--lora-accent); +} + +/* Path Preview Styles */ +.path-preview { + margin-bottom: var(--space-3); + padding: var(--space-2); + background: var(--bg-color); + border-radius: var(--border-radius-sm); + border: 1px dashed var(--border-color); +} + +.path-preview label { + display: block; + margin-bottom: 8px; + color: var(--text-color); + font-size: 0.9em; + opacity: 0.8; +} + +.path-display { + padding: var(--space-1); + color: var(--text-color); + font-family: monospace; + font-size: 0.9em; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-all; + opacity: 0.85; + background: var(--lora-surface); + border-radius: var(--border-radius-xs); +} + +/* Dark theme adjustments */ +[data-theme="dark"] .version-item { + background: var(--lora-surface); +} + +[data-theme="dark"] .local-path { + background: var(--lora-surface); + border-color: var(--lora-border); +} \ No newline at end of file diff --git a/static/css/components/lora-modal.css b/static/css/components/lora-modal.css new file mode 100644 index 00000000..ba20c0b8 --- /dev/null +++ b/static/css/components/lora-modal.css @@ -0,0 +1,417 @@ + +/* Lora Modal Header */ +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-3); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--lora-border); +} + +/* Info Grid */ +.info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--space-2); + margin-bottom: var(--space-3); +} + +.info-item { + padding: var(--space-2); + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); +} + +.info-item.full-width { + grid-column: 1 / -1; +} + +.info-item label { + display: block; + font-size: 0.85em; + color: var(--text-color); + opacity: 0.8; + margin-bottom: 4px; +} + +.info-item span { + color: var(--text-color); + word-break: break-word; +} + +.info-item.usage-tips, +.info-item.notes { + grid-column: 1 / -1 !important; /* Make notes section full width */ +} + +/* Add specific styles for notes content */ +.info-item.notes .editable-field [contenteditable] { + min-height: 60px; /* Increase height for multiple lines */ + max-height: 150px; /* Limit maximum height */ + overflow-y: auto; /* Add scrolling for long content */ + white-space: pre-wrap; /* Preserve line breaks */ + line-height: 1.5; /* Improve readability */ + padding: 8px 12px; /* Slightly increase padding */ +} + +.file-path { + font-family: monospace; + font-size: 0.9em; +} + +.description-text { + line-height: 1.5; + max-height: 100px; + overflow-y: auto; +} + +/* Showcase Section */ +.showcase-section { + position: relative; + margin-top: var(--space-4); +} + +.carousel { + transition: max-height 0.3s ease-in-out; + overflow: hidden; +} + +.carousel.collapsed { + max-height: 0; +} + +.carousel-container { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.media-wrapper { + position: relative; + width: 100%; + background: var(--lora-surface); + margin-bottom: var(--space-2); +} + +.media-wrapper:last-child { + margin-bottom: 0; +} + +.media-wrapper img, +.media-wrapper video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; +} + +/* Scroll Indicator */ +.scroll-indicator { + cursor: pointer; + padding: var(--space-2); + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-bottom: var(--space-2); + transition: background-color 0.2s, transform 0.2s; +} + +.scroll-indicator:hover { + background: oklch(var(--lora-accent) / 0.1); + transform: translateY(-1px); +} + +.scroll-indicator span { + font-size: 0.9em; + color: var(--text-color); +} + +.lazy { + opacity: 0; + transition: opacity 0.3s; +} + +.lazy[src] { + opacity: 1; +} + +/* Update Trigger Words styles */ +.info-item.trigger-words { + padding: var(--space-2); + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); +} + +.trigger-words-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: flex-start; + margin-top: var(--space-1); +} + +/* Update Trigger Words styles */ +.trigger-word-tag { + display: inline-flex; + align-items: center; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + padding: 4px 8px; + cursor: pointer; + transition: all 0.2s ease; + gap: 6px; +} + +/* Update trigger word content color to use theme accent */ +.trigger-word-content { + color: var(--lora-accent) !important; /* Override general span color */ + font-size: 0.85em; + line-height: 1.4; + word-break: break-word; +} + +/* Keep the hover effect using accent color */ +.trigger-word-tag:hover { + background: oklch(var(--lora-accent) / 0.1); + border-color: var(--lora-accent); +} + +.trigger-word-copy { + display: flex; + align-items: center; + color: var(--text-color); + opacity: 0.5; + flex-shrink: 0; + transition: opacity 0.2s; +} + +/* Editable Fields */ +.editable-field { + position: relative; + display: flex; + gap: 8px; + align-items: flex-start; +} + +.editable-field [contenteditable] { + flex: 1; + min-height: 24px; + padding: 4px 8px; + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + font-size: 0.9em; + line-height: 1.4; + color: var(--text-color); + transition: border-color 0.2s; + word-break: break-word; +} + +.editable-field [contenteditable]:focus { + outline: none; + border-color: var(--lora-accent); + background: var(--bg-color); +} + +.editable-field [contenteditable]:empty::before { + content: attr(data-placeholder); + color: var(--text-color); + opacity: 0.5; +} + +.save-btn { + padding: 4px 8px; + background: var(--lora-accent); + border: none; + border-radius: var(--border-radius-xs); + color: white; + cursor: pointer; + transition: opacity 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.save-btn:hover { + opacity: 0.9; +} + +.save-btn i { + font-size: 0.9em; +} + +@media (max-width: 640px) { + .info-item.usage-tips, + .info-item.notes { + grid-column: 1 / -1; + } +} + +/* 修改 back-to-top 按钮样式,使其固定在 modal 内部 */ +.modal-content .back-to-top { + position: sticky; /* 改用 sticky 定位 */ + float: right; /* 使用 float 确保按钮在右侧 */ + bottom: 20px; /* 距离底部的距离 */ + margin-right: 20px; /* 右侧间距 */ + margin-top: -56px; /* 负边距确保不占用额外空间 */ + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--text-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0; + visibility: hidden; + transform: translateY(10px); + transition: all 0.3s ease; + z-index: 10; +} + +.modal-content .back-to-top.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.modal-content .back-to-top:hover { + background: var(--lora-accent); + color: white; + transform: translateY(-2px); +} + +/* Update Preset Controls styles */ +.preset-controls { + display: flex; + gap: var(--space-2); + margin-bottom: var(--space-2); +} + +.preset-controls select, +.preset-controls input { + padding: var(--space-1); + background: var(--bg-color); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + color: var(--text-color); +} + +.preset-tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-1); +} + +.preset-tag { + display: flex; + align-items: center; + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + padding: calc(var(--space-1) * 0.5) var(--space-1); + gap: var(--space-1); + transition: all 0.2s ease; +} + +.preset-tag span { + color: var(--lora-accent); + font-size: 0.9em; +} + +.preset-tag i { + color: var(--text-color); + opacity: 0.5; + cursor: pointer; + transition: all 0.2s ease; +} + +.preset-tag:hover { + background: oklch(var(--lora-accent) / 0.1); + border-color: var(--lora-accent); +} + +.preset-tag i:hover { + color: var(--lora-error); + opacity: 1; +} + +.add-preset-btn { + padding: calc(var(--space-1) * 0.5) var(--space-2); + background: var(--lora-accent); + color: var(--lora-text); + border: none; + border-radius: var(--border-radius-xs); + cursor: pointer; + transition: opacity 0.2s; +} + +.add-preset-btn:hover { + opacity: 0.9; +} + +/* File name copy styles */ +.file-name-wrapper { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px; + border-radius: var(--border-radius-xs); + transition: background-color 0.2s; +} + +.file-name-wrapper:hover { + background: oklch(var(--lora-accent) / 0.1); +} + +.file-name-wrapper i { + color: var(--text-color); + opacity: 0.5; + transition: opacity 0.2s; +} + +.file-name-wrapper:hover i { + opacity: 1; + color: var(--lora-accent); +} + +/* Base Model and Size combined styles */ +.info-item.base-size { + display: flex; + gap: var(--space-3); +} + +.base-wrapper { + flex: 2; /* 分配更多空间给base model */ +} + +.size-wrapper { + flex: 1; + border-left: 1px solid var(--lora-border); + padding-left: var(--space-3); +} + +.base-wrapper label, +.size-wrapper label { + display: block; + margin-bottom: 4px; +} + +.size-wrapper span { + font-family: monospace; + font-size: 0.9em; + opacity: 0.9; +} \ No newline at end of file diff --git a/static/css/components/modal.css b/static/css/components/modal.css index 099a9846..f762c4bd 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -157,71 +157,6 @@ body.modal-open { font-size: 1.5em; } -/* Download Modal Styles */ -.download-step { - margin: var(--space-2) 0; -} - -.input-group { - margin-bottom: var(--space-2); -} - -.input-group label { - display: block; - margin-bottom: 8px; - color: var(--text-color); -} - -.input-group input, -.input-group select { - width: 100%; - padding: 8px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - background: var(--bg-color); - color: var(--text-color); -} - -.error-message { - color: var(--lora-error); - font-size: 0.9em; - margin-top: 4px; -} - -.version-list { - max-height: 400px; - overflow-y: auto; - margin: var(--space-2) 0; - display: flex; - flex-direction: column; - gap: 12px; - padding: 1px; -} - -.version-item { - display: flex; - gap: var(--space-2); - padding: var(--space-2); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - cursor: pointer; - transition: all 0.2s ease; - background: var(--bg-color); - margin: 1px; - position: relative; -} - -.version-item:hover { - border-color: var(--lora-accent); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - z-index: 1; -} - -.version-item.selected { - border-color: var(--lora-accent); - background: oklch(var(--lora-accent) / 0.1); -} - .close { position: absolute; top: var(--space-2); @@ -239,108 +174,6 @@ body.modal-open { opacity: 1; } -.version-list { - max-height: 400px; - overflow-y: auto; - margin: var(--space-2) 0; - display: flex; - flex-direction: column; - gap: 12px; - padding: 1px; /* Add padding to prevent border clipping */ -} - -.version-item { - display: flex; - gap: var(--space-2); - padding: var(--space-2); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - cursor: pointer; - transition: all 0.2s ease; - background: var(--bg-color); /* Add background color */ - margin: 1px; /* Add margin to ensure hover effect visibility */ - position: relative; /* Add position context */ -} - -.version-item:hover { - border-color: var(--lora-accent); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Replace transform with shadow */ - z-index: 1; /* Ensure hover state appears above other items */ -} - -.version-item.selected { - border-color: var(--lora-accent); - background: oklch(var(--lora-accent) / 0.1); -} - -.version-thumbnail { - width: 80px; - height: 80px; - flex-shrink: 0; - border-radius: var(--border-radius-xs); - overflow: hidden; - background: var(--bg-color); -} - -.version-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.version-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 4px; -} - -.version-content h3 { - margin: 0; - font-size: 1.1em; - color: var(--text-color); -} - -.version-info { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - font-size: 0.9em; - color: var(--text-color); - opacity: 0.8; -} - -.version-info .base-model { - background: oklch(var(--lora-accent) / 0.1); - color: var(--lora-accent); - padding: 2px 8px; - border-radius: var(--border-radius-xs); -} - -.version-meta { - display: flex; - gap: 12px; - font-size: 0.85em; - color: var(--text-color); - opacity: 0.7; -} - -.version-meta span { - display: flex; - align-items: center; - gap: 4px; -} - -.folder-browser { - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - padding: var(--space-1); - max-height: 200px; - overflow-y: auto; -} - /* Settings styles */ .settings-toggle { width: 36px; @@ -465,912 +298,6 @@ body.modal-open { margin-top: 4px; } -.folder-item { - padding: 8px; - cursor: pointer; - border-radius: var(--border-radius-xs); - transition: background-color 0.2s; -} - -.folder-item:hover { - background: var(--lora-surface); -} - -.folder-item.selected { - background: oklch(var(--lora-accent) / 0.1); - border: 1px solid var(--lora-accent); -} - -/* Path Preview Styles */ -.path-preview { - margin-bottom: var(--space-3); - padding: var(--space-2); - background: var(--bg-color); - border-radius: var(--border-radius-sm); - border: 1px dashed var(--border-color); -} - -.path-preview label { - display: block; - margin-bottom: 8px; - color: var(--text-color); - font-size: 0.9em; - opacity: 0.8; -} - -.path-display { - padding: var(--space-1); - color: var(--text-color); - font-family: monospace; - font-size: 0.9em; - line-height: 1.4; - white-space: pre-wrap; - word-break: break-all; - opacity: 0.85; - background: var(--lora-surface); - border-radius: var(--border-radius-xs); -} - -.path-text { - color: var(--text-color); -} - -.path-preview { - margin-top: var(--space-2); - padding: var(--space-2); - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); -} - -.path-preview label { - display: block; - margin-bottom: 8px; - color: var(--text-color); - font-size: 0.9em; -} - -.path-display { - display: flex; - align-items: center; - gap: 8px; - padding: var(--space-1); - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - color: var (--text-color); - font-family: monospace; - font-size: 0.9em; - overflow-x: auto; - white-space: nowrap; -} - -.path-display i { - color: var(--lora-accent); - opacity: 0.8; -} - -.path-text { - color: var(--text-color); - opacity: 0.9; -} - -/* Lora Modal Header */ -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--space-3); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--lora-border); -} - -/* Info Grid */ -.info-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--space-2); - margin-bottom: var(--space-3); -} - -.info-item { - padding: var(--space-2); - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); -} - -.info-item.full-width { - grid-column: 1 / -1; -} - -.info-item label { - display: block; - font-size: 0.85em; - color: var(--text-color); - opacity: 0.8; - margin-bottom: 4px; -} - -.info-item span { - color: var(--text-color); - word-break: break-word; -} - -.info-item.usage-tips, -.info-item.notes { - grid-column: 1 / -1 !important; /* Make notes section full width */ -} - -/* Add specific styles for notes content */ -.info-item.notes .editable-field [contenteditable] { - min-height: 60px; /* Increase height for multiple lines */ - max-height: 150px; /* Limit maximum height */ - overflow-y: auto; /* Add scrolling for long content */ - white-space: pre-wrap; /* Preserve line breaks */ - line-height: 1.5; /* Improve readability */ - padding: 8px 12px; /* Slightly increase padding */ -} - -.file-path { - font-family: monospace; - font-size: 0.9em; -} - -.description-text { - line-height: 1.5; - max-height: 100px; - overflow-y: auto; -} - -/* Showcase Section */ -.showcase-section { - position: relative; - margin-top: var(--space-4); -} - -.carousel { - transition: max-height 0.3s ease-in-out; - overflow: hidden; -} - -.carousel.collapsed { - max-height: 0; -} - -.carousel-container { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.media-wrapper { - position: relative; - width: 100%; - background: var(--lora-surface); - margin-bottom: var(--space-2); -} - -.media-wrapper:last-child { - margin-bottom: 0; -} - -.media-wrapper img, -.media-wrapper video { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: contain; -} - -/* Scroll Indicator */ -.scroll-indicator { - cursor: pointer; - padding: var(--space-2); - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - margin-bottom: var(--space-2); - transition: background-color 0.2s, transform 0.2s; -} - -.scroll-indicator:hover { - background: oklch(var(--lora-accent) / 0.1); - transform: translateY(-1px); -} - -.scroll-indicator span { - font-size: 0.9em; - color: var(--text-color); -} - -.lazy { - opacity: 0; - transition: opacity 0.3s; -} - -.lazy[src] { - opacity: 1; -} - -/* Update Trigger Words styles */ -.info-item.trigger-words { - padding: var(--space-2); - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); -} - -.trigger-words-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: flex-start; - margin-top: var(--space-1); -} - -/* Update Trigger Words styles */ -.trigger-word-tag { - display: inline-flex; - align-items: center; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - padding: 4px 8px; - cursor: pointer; - transition: all 0.2s ease; - gap: 6px; -} - -/* Update trigger word content color to use theme accent */ -.trigger-word-content { - color: var(--lora-accent) !important; /* Override general span color */ - font-size: 0.85em; - line-height: 1.4; - word-break: break-word; -} - -/* Keep the hover effect using accent color */ -.trigger-word-tag:hover { - background: oklch(var(--lora-accent) / 0.1); - border-color: var(--lora-accent); -} - -.trigger-word-copy { - display: flex; - align-items: center; - color: var(--text-color); - opacity: 0.5; - flex-shrink: 0; - transition: opacity 0.2s; -} - -/* Editable Fields */ -.editable-field { - position: relative; - display: flex; - gap: 8px; - align-items: flex-start; -} - -.editable-field [contenteditable] { - flex: 1; - min-height: 24px; - padding: 4px 8px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - font-size: 0.9em; - line-height: 1.4; - color: var(--text-color); - transition: border-color 0.2s; - word-break: break-word; -} - -.editable-field [contenteditable]:focus { - outline: none; - border-color: var(--lora-accent); - background: var(--bg-color); -} - -.editable-field [contenteditable]:empty::before { - content: attr(data-placeholder); - color: var(--text-color); - opacity: 0.5; -} - -.save-btn { - padding: 4px 8px; - background: var(--lora-accent); - border: none; - border-radius: var(--border-radius-xs); - color: white; - cursor: pointer; - transition: opacity 0.2s; - display: flex; - align-items: center; - justify-content: center; -} - -.save-btn:hover { - opacity: 0.9; -} - -.save-btn i { - font-size: 0.9em; -} - -@media (max-width: 640px) { - .info-item.usage-tips, - .info-item.notes { - grid-column: 1 / -1; - } -} - -/* 修改 back-to-top 按钮样式,使其固定在 modal 内部 */ -.modal-content .back-to-top { - position: sticky; /* 改用 sticky 定位 */ - float: right; /* 使用 float 确保按钮在右侧 */ - bottom: 20px; /* 距离底部的距离 */ - margin-right: 20px; /* 右侧间距 */ - margin-top: -56px; /* 负边距确保不占用额外空间 */ - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--card-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0; - visibility: hidden; - transform: translateY(10px); - transition: all 0.3s ease; - z-index: 10; -} - -.modal-content .back-to-top.visible { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.modal-content .back-to-top:hover { - background: var(--lora-accent); - color: white; - transform: translateY(-2px); -} - -/* Update Preset Controls styles */ -.preset-controls { - display: flex; - gap: var(--space-2); - margin-bottom: var(--space-2); -} - -.preset-controls select, -.preset-controls input { - padding: var(--space-1); - background: var(--bg-color); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-xs); - color: var(--text-color); -} - -.preset-tags { - display: flex; - flex-wrap: wrap; - gap: var(--space-1); -} - -.preset-tag { - display: flex; - align-items: center; - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-xs); - padding: calc(var(--space-1) * 0.5) var(--space-1); - gap: var(--space-1); - transition: all 0.2s ease; -} - -.preset-tag span { - color: var(--lora-accent); - font-size: 0.9em; -} - -.preset-tag i { - color: var(--text-color); - opacity: 0.5; - cursor: pointer; - transition: all 0.2s ease; -} - -.preset-tag:hover { - background: oklch(var(--lora-accent) / 0.1); - border-color: var(--lora-accent); -} - -.preset-tag i:hover { - color: var(--lora-error); - opacity: 1; -} - -.add-preset-btn { - padding: calc(var(--space-1) * 0.5) var(--space-2); - background: var(--lora-accent); - color: var(--lora-text); - border: none; - border-radius: var(--border-radius-xs); - cursor: pointer; - transition: opacity 0.2s; -} - -.add-preset-btn:hover { - opacity: 0.9; -} - -/* File name copy styles */ -.file-name-wrapper { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - padding: 4px; - border-radius: var(--border-radius-xs); - transition: background-color 0.2s; -} - -.file-name-wrapper:hover { - background: oklch(var(--lora-accent) / 0.1); -} - -.file-name-wrapper i { - color: var(--text-color); - opacity: 0.5; - transition: opacity 0.2s; -} - -.file-name-wrapper:hover i { - opacity: 1; - color: var(--lora-accent); -} - -/* Location and Size combined styles */ -.info-item.location-size { - display: block; -} - -/* Base Model and Size combined styles */ -.info-item.base-size { - display: flex; - gap: var(--space-3); -} - -.base-wrapper { - flex: 2; /* 分配更多空间给base model */ -} - -.size-wrapper { - flex: 1; - border-left: 1px solid var(--lora-border); - padding-left: var(--space-3); -} - -.base-wrapper label, -.size-wrapper label { - display: block; - margin-bottom: 4px; -} - -.size-wrapper span { - font-family: monospace; - font-size: 0.9em; - opacity: 0.9; -} - -/* Support Modal Styles */ -.support-modal { - max-width: 550px; -} - -.support-header { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-3); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--lora-border); -} - -.support-icon { - font-size: 1.8em; - color: var(--lora-error); - animation: pulse 1.5s infinite; -} - -@keyframes pulse { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - } -} - -.support-content { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.support-content > p { - font-size: 1.1em; - text-align: center; - margin-bottom: var(--space-2); -} - -.support-section { - background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ - border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ - border-radius: var(--border-radius-sm); - padding: var(--space-3); - margin-bottom: var(--space-2); -} - -.support-section h3 { - display: flex; - align-items: center; - gap: 10px; - margin-top: 0; - margin-bottom: var(--space-1); - font-size: 1.1em; - color: var(--lora-accent); -} - -.support-section h3 i { - opacity: 0.8; -} - -.support-section p { - margin-top: 6px; - margin-bottom: var(--space-2); - color: var(--text-color); - opacity: 0.9; -} - -.support-links { - display: flex; - gap: var(--space-2); - margin-top: var(--space-2); -} - -.social-link { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 16px; - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); - text-decoration: none; - color: var(--text-color); - transition: all 0.2s ease; -} - -.social-link:hover { - background: var(--lora-accent); - color: white; - transform: translateY(-2px); -} - -.kofi-button { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - padding: 10px 20px; - background: #FF5E5B; - color: white; - border-radius: var(--border-radius-sm); - text-decoration: none; - font-weight: 500; - transition: all 0.2s ease; - margin-top: var(--space-1); -} - -.kofi-button:hover { - background: #E04946; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.support-footer { - text-align: center; - margin-top: var(--space-2); - font-style: italic; - color: var(--text-color); -} - -/* Add support toggle button style */ -.support-toggle { - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--card-bg); - border: 1px solid var(--border-color); - color: var(--lora-error); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; -} - -.support-toggle:hover { - background: var(--lora-accent); - color: white; - transform: translateY(-2px); -} - -.support-toggle i { - font-size: 1.1em; - position: relative; - top: 1px; - left: -0.5px; -} - -@media (max-width: 480px) { - .support-links { - flex-direction: column; - } -} - -/* Update Modal Styles */ -.update-modal { - max-width: 600px; -} - -.update-header { - display: flex; - align-items: center; - gap: var(--space-2); - margin-bottom: var(--space-3); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--lora-border); -} - -.update-icon { - font-size: 1.8em; - color: var(--lora-accent); - animation: bounce 1.5s infinite; -} - -@keyframes bounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-5px); - } -} - -.update-content { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.update-info { - display: flex; - justify-content: space-between; - align-items: center; - background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ - border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ - border-radius: var(--border-radius-sm); - padding: var(--space-3); -} - -.version-info { - display: flex; - flex-direction: column; - gap: 8px; -} - -.current-version, .new-version { - display: flex; - align-items: center; - gap: 10px; -} - -.label { - font-size: 0.9em; - color: var(--text-color); - opacity: 0.8; -} - -.version-number { - font-family: monospace; - font-weight: 600; -} - -.new-version .version-number { - color: var(--lora-accent); -} - -.update-link { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 16px; - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); - text-decoration: none; - color: var(--text-color); - transition: all 0.2s ease; -} - -.update-link:hover { - background: var(--lora-accent); - color: white; - transform: translateY(-2px); -} - -.changelog-section { - background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ - border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ - border-radius: var(--border-radius-sm); - padding: var(--space-3); -} - -.changelog-section h3 { - margin-top: 0; - margin-bottom: var(--space-2); - color: var(--lora-accent); - font-size: 1.1em; -} - -.changelog-content { - max-height: 300px; /* Increased height since we removed instructions */ - overflow-y: auto; -} - -.changelog-item { - margin-bottom: var(--space-2); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--lora-border); -} - -.changelog-item:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; -} - -.changelog-item h4 { - margin-top: 0; - margin-bottom: 8px; - font-size: 1em; - color: var(--text-color); -} - -.changelog-item ul { - margin: 0; - padding-left: 20px; -} - -.changelog-item li { - margin-bottom: 4px; - color: var(--text-color); -} - -@media (max-width: 480px) { - .update-info { - flex-direction: column; - gap: var(--space-2); - } - - .version-info { - width: 100%; - } -} - -/* Update preferences section */ -.update-preferences { - border-top: 1px solid var(--lora-border); - margin-top: var(--space-2); - padding-top: var(--space-2); -} - -/* Toggle switch styles */ -.toggle-switch { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - user-select: none; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; - position: absolute; -} - -.toggle-slider { - position: relative; - display: inline-block; - width: 40px; - height: 20px; - background-color: var(--border-color); - border-radius: 20px; - transition: .4s; - flex-shrink: 0; -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 16px; - width: 16px; - left: 2px; - bottom: 2px; - background-color: white; - border-radius: 50%; - transition: .4s; -} - -input:checked + .toggle-slider { - background-color: var(--lora-accent); -} - -input:checked + .toggle-slider:before { - transform: translateX(20px); -} - -.toggle-label { - font-size: 0.9em; - color: var(--text-color); -} - -/* Civitai link styles */ -.civitai-link { - display: flex; - align-items: center; - gap: 8px; -} - -.civitai-icon { - width: 20px; - height: 20px; - color: #1b98e4; /* Civitai brand blue color */ -} - -.social-link:hover .civitai-icon { - color: white; /* Icon color changes to white on hover */ -} - -/* 增强hover状态的视觉反馈 */ -.social-link:hover, -.update-link:hover, -.folder-item:hover { - border-color: var(--lora-accent); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - /* 调整深色主题 */ [data-theme="dark"] .modal-content { background: var(--lora-surface); diff --git a/static/css/components/support-modal.css b/static/css/components/support-modal.css new file mode 100644 index 00000000..04ecb115 --- /dev/null +++ b/static/css/components/support-modal.css @@ -0,0 +1,184 @@ +/* Support Modal Styles */ +.support-modal { + max-width: 550px; +} + +.support-header { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-3); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--lora-border); +} + +.support-icon { + font-size: 1.8em; + color: var(--lora-error); + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +.support-content { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.support-content > p { + font-size: 1.1em; + text-align: center; + margin-bottom: var(--space-2); +} + +.support-section { + background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ + border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ + border-radius: var(--border-radius-sm); + padding: var(--space-3); + margin-bottom: var(--space-2); +} + +.support-section h3 { + display: flex; + align-items: center; + gap: 10px; + margin-top: 0; + margin-bottom: var(--space-1); + font-size: 1.1em; + color: var(--lora-accent); +} + +.support-section h3 i { + opacity: 0.8; +} + +.support-section p { + margin-top: 6px; + margin-bottom: var(--space-2); + color: var(--text-color); + opacity: 0.9; +} + +.support-links { + display: flex; + gap: var(--space-2); + margin-top: var(--space-2); +} + +.social-link { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + text-decoration: none; + color: var(--text-color); + transition: all 0.2s ease; +} + +.social-link:hover { + background: var(--lora-accent); + color: white; + transform: translateY(-2px); +} + +.kofi-button { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px 20px; + background: #FF5E5B; + color: white; + border-radius: var(--border-radius-sm); + text-decoration: none; + font-weight: 500; + transition: all 0.2s ease; + margin-top: var(--space-1); +} + +.kofi-button:hover { + background: #E04946; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.support-footer { + text-align: center; + margin-top: var(--space-2); + font-style: italic; + color: var(--text-color); +} + +/* Add support toggle button style */ +.support-toggle { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--card-bg); + border: 1px solid var(--border-color); + color: var(--lora-error); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + +.support-toggle:hover { + background: var(--lora-accent); + color: white; + transform: translateY(-2px); +} + +.support-toggle i { + font-size: 1.1em; + position: relative; + top: 1px; + left: -0.5px; +} + +@media (max-width: 480px) { + .support-links { + flex-direction: column; + } +} + +/* Civitai link styles */ +.civitai-link { + display: flex; + align-items: center; + gap: 8px; +} + +.civitai-icon { + width: 20px; + height: 20px; + color: #1b98e4; /* Civitai brand blue color */ +} + +.social-link:hover .civitai-icon { + color: white; /* Icon color changes to white on hover */ +} + +/* 增强hover状态的视觉反馈 */ +.social-link:hover, +.update-link:hover, +.folder-item:hover { + border-color: var(--lora-accent); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/static/css/components/update-modal.css b/static/css/components/update-modal.css new file mode 100644 index 00000000..28d185bd --- /dev/null +++ b/static/css/components/update-modal.css @@ -0,0 +1,208 @@ +/* Update Modal Styles */ +.update-modal { + max-width: 600px; +} + +.update-header { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-3); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--lora-border); +} + +.update-icon { + font-size: 1.8em; + color: var(--lora-accent); + animation: bounce 1.5s infinite; +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +.update-content { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.update-info { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ + border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ + border-radius: var(--border-radius-sm); + padding: var(--space-3); +} + +.version-info { + display: flex; + flex-direction: column; + gap: 8px; +} + +.current-version, .new-version { + display: flex; + align-items: center; + gap: 10px; +} + +.label { + font-size: 0.9em; + color: var(--text-color); + opacity: 0.8; +} + +.version-number { + font-family: monospace; + font-weight: 600; +} + +.new-version .version-number { + color: var(--lora-accent); +} + +.update-link { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + text-decoration: none; + color: var(--text-color); + transition: all 0.2s ease; +} + +.update-link:hover { + background: var(--lora-accent); + color: white; + transform: translateY(-2px); +} + +.changelog-section { + background: rgba(0, 0, 0, 0.02); /* 轻微的灰色背景 */ + border: 1px solid rgba(0, 0, 0, 0.08); /* 更明显的边框 */ + border-radius: var(--border-radius-sm); + padding: var(--space-3); +} + +.changelog-section h3 { + margin-top: 0; + margin-bottom: var(--space-2); + color: var(--lora-accent); + font-size: 1.1em; +} + +.changelog-content { + max-height: 300px; /* Increased height since we removed instructions */ + overflow-y: auto; +} + +.changelog-item { + margin-bottom: var(--space-2); + padding-bottom: var(--space-2); + border-bottom: 1px solid var(--lora-border); +} + +.changelog-item:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.changelog-item h4 { + margin-top: 0; + margin-bottom: 8px; + font-size: 1em; + color: var(--text-color); +} + +.changelog-item ul { + margin: 0; + padding-left: 20px; +} + +.changelog-item li { + margin-bottom: 4px; + color: var(--text-color); +} + +@media (max-width: 480px) { + .update-info { + flex-direction: column; + gap: var(--space-2); + } + + .version-info { + width: 100%; + } +} + +/* Update preferences section */ +.update-preferences { + border-top: 1px solid var(--lora-border); + margin-top: var(--space-2); + padding-top: var(--space-2); +} + +/* Toggle switch styles */ +.toggle-switch { + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + user-select: none; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.toggle-slider { + position: relative; + display: inline-block; + width: 40px; + height: 20px; + background-color: var(--border-color); + border-radius: 20px; + transition: .4s; + flex-shrink: 0; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + border-radius: 50%; + transition: .4s; +} + +input:checked + .toggle-slider { + background-color: var(--lora-accent); +} + +input:checked + .toggle-slider:before { + transform: translateX(20px); +} + +.toggle-label { + font-size: 0.9em; + color: var(--text-color); +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index a0a79844..1b2c249a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -7,9 +7,13 @@ /* Import Components */ @import 'components/card.css'; @import 'components/modal.css'; +@import 'components/download-modal.css'; @import 'components/toast.css'; @import 'components/loading.css'; @import 'components/menu.css'; +@import 'components/update-modal.css'; +@import 'components/lora-modal.css'; +@import 'components/support-modal.css'; .initialization-notice { display: flex; diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index 59075f72..08470889 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -71,7 +71,6 @@ export class DownloadManager { const errorElement = document.getElementById('urlError'); try { - // Show loading while fetching versions this.loadingManager.showSimpleLoading('Fetching model versions...'); const modelId = this.extractModelId(url); @@ -98,7 +97,6 @@ export class DownloadManager { } catch (error) { errorElement.textContent = error.message; } finally { - // Hide loading when done this.loadingManager.hide(); } } @@ -120,19 +118,31 @@ export class DownloadManager { const versionList = document.getElementById('versionList'); versionList.innerHTML = this.versions.map(version => { - // Find first image (skip videos) const firstImage = version.images?.find(img => !img.url.endsWith('.mp4')); const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png'; - const fileSize = (version.files[0]?.sizeKB / 1024).toFixed(2); // Convert to MB + const fileSize = (version.files[0]?.sizeKB / 1024).toFixed(2); + + const existsLocally = version.files[0]?.existsLocally; + const localPath = version.files[0]?.localPath; + + // 更新本地状态指示器为badge样式 + const localStatus = existsLocally ? + `
+ In Library +
${localPath}
+
` : ''; return ` -
Version preview
-

${version.name}

+
+

${version.name}

+ ${localStatus} +
${version.baseModel ? `
${version.baseModel}
` : ''}
@@ -150,6 +160,12 @@ export class DownloadManager { this.currentVersion = this.versions.find(v => v.id.toString() === versionId.toString()); if (!this.currentVersion) return; + // Check if version exists locally + const existsLocally = this.currentVersion.files[0]?.existsLocally; + if (existsLocally) { + showToast('This version already exists in your library', 'info'); + } + document.querySelectorAll('.version-item').forEach(item => { item.classList.toggle('selected', item.querySelector('h3').textContent === this.currentVersion.name); });