mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-17 16:09:25 -03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75298a402f | ||
|
|
92b5efd414 | ||
|
|
33ee392b7b | ||
|
|
5237f8b7dc | ||
|
|
5107313fd1 | ||
|
|
95bbc66919 | ||
|
|
e268e59419 | ||
|
|
547e1f9498 |
@@ -1,153 +0,0 @@
|
|||||||
# Recipe Batch Import Feature Design
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Enable users to import multiple images as recipes in a single operation, rather than processing them individually. This feature addresses the need for efficient bulk recipe creation from existing image collections.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Frontend │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ BatchImportManager.js │
|
|
||||||
│ ├── InputCollector (收集URL列表/目录路径) │
|
|
||||||
│ ├── ConcurrencyController (自适应并发控制) │
|
|
||||||
│ ├── ProgressTracker (进度追踪) │
|
|
||||||
│ └── ResultAggregator (结果汇总) │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ batch_import_modal.html │
|
|
||||||
│ └── 批量导入UI组件 │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ batch_import_progress.css │
|
|
||||||
│ └── 进度显示样式 │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Backend │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ py/routes/handlers/recipe_handlers.py │
|
|
||||||
│ ├── start_batch_import() - 启动批量导入 │
|
|
||||||
│ ├── get_batch_import_progress() - 查询进度 │
|
|
||||||
│ └── cancel_batch_import() - 取消导入 │
|
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
|
||||||
│ py/services/batch_import_service.py │
|
|
||||||
│ ├── 自适应并发执行 │
|
|
||||||
│ ├── 结果汇总 │
|
|
||||||
│ └── WebSocket进度广播 │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
| 端点 | 方法 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `/api/lm/recipes/batch-import/start` | POST | 启动批量导入,返回 operation_id |
|
|
||||||
| `/api/lm/recipes/batch-import/progress` | GET | 查询进度状态 |
|
|
||||||
| `/api/lm/recipes/batch-import/cancel` | POST | 取消导入 |
|
|
||||||
|
|
||||||
## Backend Implementation Details
|
|
||||||
|
|
||||||
### BatchImportService
|
|
||||||
|
|
||||||
Location: `py/services/batch_import_service.py`
|
|
||||||
|
|
||||||
Key classes:
|
|
||||||
- `BatchImportItem`: Dataclass for individual import item
|
|
||||||
- `BatchImportProgress`: Dataclass for tracking progress
|
|
||||||
- `BatchImportService`: Main service class
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Adaptive concurrency control (adjusts based on success/failure rate)
|
|
||||||
- WebSocket progress broadcasting
|
|
||||||
- Graceful error handling (individual failures don't stop the batch)
|
|
||||||
- Result aggregation
|
|
||||||
|
|
||||||
### WebSocket Message Format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "batch_import_progress",
|
|
||||||
"operation_id": "xxx",
|
|
||||||
"total": 50,
|
|
||||||
"completed": 23,
|
|
||||||
"success": 21,
|
|
||||||
"failed": 2,
|
|
||||||
"skipped": 0,
|
|
||||||
"current_item": "image_024.png",
|
|
||||||
"status": "running"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Input Types
|
|
||||||
|
|
||||||
1. **URL List**: Array of URLs (http/https)
|
|
||||||
2. **Local Paths**: Array of local file paths
|
|
||||||
3. **Directory**: Path to directory with optional recursive flag
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Invalid URLs/paths: Skip and record error
|
|
||||||
- Download failures: Record error, continue
|
|
||||||
- Metadata extraction failures: Mark as "no metadata"
|
|
||||||
- Duplicate detection: Option to skip duplicates
|
|
||||||
|
|
||||||
## Frontend Implementation Details (TODO)
|
|
||||||
|
|
||||||
### UI Components
|
|
||||||
|
|
||||||
1. **BatchImportModal**: Main modal with tabs for URLs/Directory input
|
|
||||||
2. **ProgressDisplay**: Real-time progress bar and status
|
|
||||||
3. **ResultsSummary**: Final results with success/failure breakdown
|
|
||||||
|
|
||||||
### Adaptive Concurrency Controller
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class AdaptiveConcurrencyController {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.minConcurrency = options.minConcurrency || 1;
|
|
||||||
this.maxConcurrency = options.maxConcurrency || 5;
|
|
||||||
this.currentConcurrency = options.initialConcurrency || 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustConcurrency(taskDuration, success) {
|
|
||||||
if (success && taskDuration < 1000 && this.currentConcurrency < this.maxConcurrency) {
|
|
||||||
this.currentConcurrency = Math.min(this.currentConcurrency + 1, this.maxConcurrency);
|
|
||||||
}
|
|
||||||
if (!success || taskDuration > 10000) {
|
|
||||||
this.currentConcurrency = Math.max(this.currentConcurrency - 1, this.minConcurrency);
|
|
||||||
}
|
|
||||||
return this.currentConcurrency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
Backend (implemented):
|
|
||||||
├── py/services/batch_import_service.py # 后端服务
|
|
||||||
├── py/routes/handlers/batch_import_handler.py # API处理器 (added to recipe_handlers.py)
|
|
||||||
├── tests/services/test_batch_import_service.py # 单元测试
|
|
||||||
└── tests/routes/test_batch_import_routes.py # API集成测试
|
|
||||||
|
|
||||||
Frontend (TODO):
|
|
||||||
├── static/js/managers/BatchImportManager.js # 主管理器
|
|
||||||
├── static/js/managers/batch/ # 子模块
|
|
||||||
│ ├── ConcurrencyController.js # 并发控制
|
|
||||||
│ ├── ProgressTracker.js # 进度追踪
|
|
||||||
│ └── ResultAggregator.js # 结果汇总
|
|
||||||
├── static/css/components/batch-import-modal.css # 样式
|
|
||||||
└── templates/components/batch_import_modal.html # Modal模板
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Status
|
|
||||||
|
|
||||||
- [x] Backend BatchImportService
|
|
||||||
- [x] Backend API handlers
|
|
||||||
- [x] WebSocket progress broadcasting
|
|
||||||
- [x] Unit tests
|
|
||||||
- [x] Integration tests
|
|
||||||
- [ ] Frontend BatchImportManager
|
|
||||||
- [ ] Frontend UI components
|
|
||||||
- [ ] E2E tests
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,3 +28,6 @@ vue-widgets/dist/
|
|||||||
|
|
||||||
# Hypothesis test cache
|
# Hypothesis test cache
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
|
|
||||||
|
# Working/research notes (not committed)
|
||||||
|
.docs/
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "Dieser Tag existiert bereits"
|
"duplicate": "Dieser Tag existiert bereits"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "Tastatur-Navigation:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "Eine Seite nach oben scrollen",
|
|
||||||
"pageDown": "Eine Seite nach unten scrollen",
|
|
||||||
"home": "Zum Anfang springen",
|
|
||||||
"end": "Zum Ende springen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "Initialisierung",
|
"title": "Initialisierung",
|
||||||
"message": "Ihr Arbeitsbereich wird vorbereitet...",
|
"message": "Ihr Arbeitsbereich wird vorbereitet...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "This tag already exists"
|
"duplicate": "This tag already exists"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "Keyboard Navigation:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "Scroll up one page",
|
|
||||||
"pageDown": "Scroll down one page",
|
|
||||||
"home": "Jump to top",
|
|
||||||
"end": "Jump to bottom"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "Initializing",
|
"title": "Initializing",
|
||||||
"message": "Preparing your workspace...",
|
"message": "Preparing your workspace...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "Esta etiqueta ya existe"
|
"duplicate": "Esta etiqueta ya existe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "Navegación por teclado:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "Desplazar hacia arriba una página",
|
|
||||||
"pageDown": "Desplazar hacia abajo una página",
|
|
||||||
"home": "Saltar al inicio",
|
|
||||||
"end": "Saltar al final"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "Inicializando",
|
"title": "Inicializando",
|
||||||
"message": "Preparando tu espacio de trabajo...",
|
"message": "Preparando tu espacio de trabajo...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "Ce tag existe déjà"
|
"duplicate": "Ce tag existe déjà"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "Navigation au clavier :",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "Défiler d'une page vers le haut",
|
|
||||||
"pageDown": "Défiler d'une page vers le bas",
|
|
||||||
"home": "Aller en haut",
|
|
||||||
"end": "Aller en bas"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "Initialisation",
|
"title": "Initialisation",
|
||||||
"message": "Préparation de votre espace de travail...",
|
"message": "Préparation de votre espace de travail...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "תגית זו כבר קיימת"
|
"duplicate": "תגית זו כבר קיימת"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "ניווט במקלדת:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "גלול עמוד אחד למעלה",
|
|
||||||
"pageDown": "גלול עמוד אחד למטה",
|
|
||||||
"home": "קפוץ להתחלה",
|
|
||||||
"end": "קפוץ לסוף"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "מאתחל",
|
"title": "מאתחל",
|
||||||
"message": "מכין את סביבת העבודה שלך...",
|
"message": "מכין את סביבת העבודה שלך...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "このタグは既に存在します"
|
"duplicate": "このタグは既に存在します"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "キーボードナビゲーション:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "1ページ上にスクロール",
|
|
||||||
"pageDown": "1ページ下にスクロール",
|
|
||||||
"home": "トップにジャンプ",
|
|
||||||
"end": "ボトムにジャンプ"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "初期化中",
|
"title": "初期化中",
|
||||||
"message": "ワークスペースを準備中...",
|
"message": "ワークスペースを準備中...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "이 태그는 이미 존재합니다"
|
"duplicate": "이 태그는 이미 존재합니다"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "키보드 내비게이션:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "한 페이지 위로 스크롤",
|
|
||||||
"pageDown": "한 페이지 아래로 스크롤",
|
|
||||||
"home": "맨 위로 이동",
|
|
||||||
"end": "맨 아래로 이동"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "초기화 중",
|
"title": "초기화 중",
|
||||||
"message": "작업공간을 준비하고 있습니다...",
|
"message": "작업공간을 준비하고 있습니다...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "Этот тег уже существует"
|
"duplicate": "Этот тег уже существует"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "Навигация с клавиатуры:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "Прокрутить на страницу вверх",
|
|
||||||
"pageDown": "Прокрутить на страницу вниз",
|
|
||||||
"home": "Перейти к началу",
|
|
||||||
"end": "Перейти к концу"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "Инициализация",
|
"title": "Инициализация",
|
||||||
"message": "Подготовка вашего рабочего пространства...",
|
"message": "Подготовка вашего рабочего пространства...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "该标签已存在"
|
"duplicate": "该标签已存在"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "键盘导航:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "向上一页滚动",
|
|
||||||
"pageDown": "向下一页滚动",
|
|
||||||
"home": "跳到顶部",
|
|
||||||
"end": "跳到底部"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "初始化",
|
"title": "初始化",
|
||||||
"message": "正在准备你的工作空间...",
|
"message": "正在准备你的工作空间...",
|
||||||
|
|||||||
@@ -1424,15 +1424,6 @@
|
|||||||
"duplicate": "此標籤已存在"
|
"duplicate": "此標籤已存在"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"keyboard": {
|
|
||||||
"navigation": "鍵盤導覽:",
|
|
||||||
"shortcuts": {
|
|
||||||
"pageUp": "向上捲動一頁",
|
|
||||||
"pageDown": "向下捲動一頁",
|
|
||||||
"home": "跳至頂部",
|
|
||||||
"end": "跳至底部"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"initialization": {
|
"initialization": {
|
||||||
"title": "初始化",
|
"title": "初始化",
|
||||||
"message": "正在準備您的工作區...",
|
"message": "正在準備您的工作區...",
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ async def calculate_sha256(file_path: str) -> str:
|
|||||||
Uses ``posix_fadvise`` with ``POSIX_FADV_DONTNEED`` to avoid polluting the OS page
|
Uses ``posix_fadvise`` with ``POSIX_FADV_DONTNEED`` to avoid polluting the OS page
|
||||||
cache — critical on WSL where cached file pages live inside the VM and are not
|
cache — critical on WSL where cached file pages live inside the VM and are not
|
||||||
accounted for in guest ``used`` memory, causing VmmemWSL to balloon.
|
accounted for in guest ``used`` memory, causing VmmemWSL to balloon.
|
||||||
|
|
||||||
|
On Windows/macOS where ``posix_fadvise`` is not available the hint is silently
|
||||||
|
skipped.
|
||||||
"""
|
"""
|
||||||
sha256_hash = hashlib.sha256()
|
sha256_hash = hashlib.sha256()
|
||||||
chunk_size = _get_hash_chunk_size_bytes()
|
chunk_size = _get_hash_chunk_size_bytes()
|
||||||
@@ -48,7 +51,9 @@ async def calculate_sha256(file_path: str) -> str:
|
|||||||
sha256_hash.update(byte_block)
|
sha256_hash.update(byte_block)
|
||||||
# Evict pages after reading so the data doesn't linger in the kernel page
|
# Evict pages after reading so the data doesn't linger in the kernel page
|
||||||
# cache — on WSL this otherwise appears as unreclaimable VmmemWSL growth.
|
# cache — on WSL this otherwise appears as unreclaimable VmmemWSL growth.
|
||||||
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
|
# Guard against platforms (Windows, macOS) that lack posix_fadvise.
|
||||||
|
if hasattr(os, "posix_fadvise") and hasattr(os, "POSIX_FADV_DONTNEED"):
|
||||||
|
os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
|
||||||
return sha256_hash.hexdigest()
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "1.1.2"
|
version = "1.1.3"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
/* Keyboard navigation indicator and help */
|
|
||||||
.keyboard-nav-hint {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: help;
|
|
||||||
transition: var(--transition-base);
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-nav-hint:hover {
|
|
||||||
background: var(--lora-accent);
|
|
||||||
color: white;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-nav-hint i {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip styling */
|
|
||||||
.tooltip {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltiptext {
|
|
||||||
visibility: hidden;
|
|
||||||
width: 240px;
|
|
||||||
background-color: var(--lora-surface);
|
|
||||||
color: var(--text-color);
|
|
||||||
text-align: center;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
padding: 8px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9999; /* Ensure tooltip appears above cards */
|
|
||||||
right: 120%; /* Position tooltip to the left of the icon */
|
|
||||||
top: 50%; /* Vertically center */
|
|
||||||
transform: translateY(-15%); /* Vertically center */
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
border: 1px solid var(--lora-border);
|
|
||||||
font-size: 0.85em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltiptext::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 50%; /* Vertically center arrow */
|
|
||||||
left: 100%; /* Arrow on the right side */
|
|
||||||
margin-top: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent transparent transparent var(--lora-border); /* Arrow points right */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:hover .tooltiptext {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keyboard shortcuts table */
|
|
||||||
.keyboard-shortcuts {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcuts td {
|
|
||||||
padding: 4px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keyboard-shortcuts td:first-child {
|
|
||||||
font-weight: bold;
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.key {
|
|
||||||
display: inline-block;
|
|
||||||
background: var(--bg-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
font-size: 0.8em;
|
|
||||||
box-shadow: var(--shadow-xs);
|
|
||||||
}
|
|
||||||
@@ -823,54 +823,107 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.range-control input[type="range"] {
|
.range-control input[type="range"] {
|
||||||
|
--range-fill: 40%;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 4px;
|
height: 6px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: var(--border-color);
|
background: linear-gradient(
|
||||||
border-radius: 2px;
|
to right,
|
||||||
|
var(--lora-accent) 0%,
|
||||||
|
var(--lora-accent) var(--range-fill),
|
||||||
|
var(--border-color) var(--range-fill),
|
||||||
|
var(--border-color) 100%
|
||||||
|
);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]:focus-visible {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.range-control input[type="range"]::-webkit-slider-thumb {
|
.range-control input[type="range"]::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid var(--lora-surface);
|
border: 2px solid var(--lora-surface);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-md);
|
||||||
transition: transform 0.15s ease;
|
transition: transform var(--transition-bounce), box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.range-control input[type="range"]::-webkit-slider-thumb:hover {
|
.range-control input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
transform: scale(1.15);
|
transform: scale(1.2);
|
||||||
|
box-shadow: var(--shadow-md), 0 0 0 4px var(--color-accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]::-webkit-slider-thumb:active {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: var(--shadow-md), 0 0 0 6px var(--color-accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]:focus-visible::-webkit-slider-thumb {
|
||||||
|
box-shadow: var(--shadow-md), 0 0 0 3px var(--color-accent-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.range-control input[type="range"]::-moz-range-thumb {
|
.range-control input[type="range"]::-moz-range-thumb {
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid var(--lora-surface);
|
border: 2px solid var(--lora-surface);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-md);
|
||||||
|
transition: transform var(--transition-bounce), box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]::-moz-range-thumb:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
box-shadow: var(--shadow-md), 0 0 0 4px var(--color-accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]::-moz-range-thumb:active {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: var(--shadow-md), 0 0 0 6px var(--color-accent-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-control input[type="range"]::-moz-range-track {
|
||||||
|
height: 6px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.range-control .range-value {
|
.range-control .range-value {
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.9em;
|
font-size: 0.85em;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-color);
|
color: var(--lora-accent);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .range-control input[type="range"] {
|
[data-theme="dark"] .range-control input[type="range"] {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--lora-accent) 0%,
|
||||||
|
var(--lora-accent) var(--range-fill),
|
||||||
|
rgba(255, 255, 255, 0.15) var(--range-fill),
|
||||||
|
rgba(255, 255, 255, 0.15) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .range-control input[type="range"]::-moz-range-track {
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
@import 'components/initialization.css';
|
@import 'components/initialization.css';
|
||||||
@import 'components/progress-panel.css';
|
@import 'components/progress-panel.css';
|
||||||
@import 'components/duplicates.css'; /* Add duplicates component */
|
@import 'components/duplicates.css'; /* Add duplicates component */
|
||||||
@import 'components/keyboard-nav.css'; /* Add keyboard navigation component */
|
|
||||||
@import 'components/statistics.css'; /* Add statistics component */
|
@import 'components/statistics.css'; /* Add statistics component */
|
||||||
@import 'components/sidebar.css'; /* Add sidebar component */
|
@import 'components/sidebar.css'; /* Add sidebar component */
|
||||||
@import 'components/media-viewer.css';
|
@import 'components/media-viewer.css';
|
||||||
|
|||||||
@@ -805,12 +805,14 @@ export class SettingsManager {
|
|||||||
|
|
||||||
// Set card blur amount slider
|
// Set card blur amount slider
|
||||||
const cardBlurAmountInput = document.getElementById('cardBlurAmount');
|
const cardBlurAmountInput = document.getElementById('cardBlurAmount');
|
||||||
|
const cardBlurValue = state.global.settings.card_blur_amount ?? 8;
|
||||||
if (cardBlurAmountInput) {
|
if (cardBlurAmountInput) {
|
||||||
cardBlurAmountInput.value = state.global.settings.card_blur_amount ?? 8;
|
cardBlurAmountInput.value = cardBlurValue;
|
||||||
|
cardBlurAmountInput.style.setProperty('--range-fill', (cardBlurValue / 20 * 100) + '%');
|
||||||
}
|
}
|
||||||
const cardBlurAmountValue = document.getElementById('cardBlurAmountValue');
|
const cardBlurAmountValue = document.getElementById('cardBlurAmountValue');
|
||||||
if (cardBlurAmountValue) {
|
if (cardBlurAmountValue) {
|
||||||
cardBlurAmountValue.textContent = `${state.global.settings.card_blur_amount ?? 8}px`;
|
cardBlurAmountValue.textContent = `${cardBlurValue}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usePortableCheckbox = document.getElementById('usePortableSettings');
|
const usePortableCheckbox = document.getElementById('usePortableSettings');
|
||||||
@@ -2070,6 +2072,9 @@ export class SettingsManager {
|
|||||||
displayEl.textContent = `${value}px`;
|
displayEl.textContent = `${value}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const max = parseInt(element.max, 10) || 20;
|
||||||
|
element.style.setProperty('--range-fill', (value / max * 100) + '%');
|
||||||
|
|
||||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
showToast('toast.settings.settingSaveFailed', { message: error.message }, 'error');
|
||||||
|
|||||||
@@ -100,30 +100,6 @@
|
|||||||
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="keyboard-nav-hint tooltip">
|
|
||||||
<i class="fas fa-keyboard"></i>
|
|
||||||
<span class="tooltiptext">
|
|
||||||
<span>{{ t('keyboard.navigation') }}</span>
|
|
||||||
<table class="keyboard-shortcuts">
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Page Up</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.pageUp') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Page Down</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.pageDown') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Home</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.home') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">End</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.end') }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="setting-control range-control">
|
<div class="setting-control range-control">
|
||||||
<input type="range" id="cardBlurAmount" min="0" max="20" value="8" step="1"
|
<input type="range" id="cardBlurAmount" min="0" max="20" value="8" step="1"
|
||||||
oninput="document.getElementById('cardBlurAmountValue').textContent = this.value + 'px'"
|
oninput="var pct = (this.value / 20) * 100; this.style.setProperty('--range-fill', pct + '%'); document.getElementById('cardBlurAmountValue').textContent = this.value + 'px'"
|
||||||
onchange="settingsManager.saveRangeSetting('cardBlurAmount', 'cardBlurAmountValue', 'card_blur_amount')">
|
onchange="settingsManager.saveRangeSetting('cardBlurAmount', 'cardBlurAmountValue', 'card_blur_amount')">
|
||||||
<span id="cardBlurAmountValue" class="range-value">8px</span>
|
<span id="cardBlurAmountValue" class="range-value">8px</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,30 +137,6 @@
|
|||||||
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="keyboard-nav-hint tooltip">
|
|
||||||
<i class="fas fa-keyboard"></i>
|
|
||||||
<span class="tooltiptext">
|
|
||||||
<span>{{ t('keyboard.navigation') }}</span>
|
|
||||||
<table class="keyboard-shortcuts">
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Page Up</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.pageUp') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Page Down</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.pageDown') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">Home</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.home') }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="key">End</span></td>
|
|
||||||
<td>{{ t('keyboard.shortcuts.end') }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user