mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-17 07:59:24 -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/
|
||||
|
||||
# Working/research notes (not committed)
|
||||
.docs/
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"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": {
|
||||
"title": "Initialisierung",
|
||||
"message": "Ihr Arbeitsbereich wird vorbereitet...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"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": {
|
||||
"title": "Initializing",
|
||||
"message": "Preparing your workspace...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"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": {
|
||||
"title": "Inicializando",
|
||||
"message": "Preparando tu espacio de trabajo...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"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": {
|
||||
"title": "Initialisation",
|
||||
"message": "Préparation de votre espace de travail...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "תגית זו כבר קיימת"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "ניווט במקלדת:",
|
||||
"shortcuts": {
|
||||
"pageUp": "גלול עמוד אחד למעלה",
|
||||
"pageDown": "גלול עמוד אחד למטה",
|
||||
"home": "קפוץ להתחלה",
|
||||
"end": "קפוץ לסוף"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "מאתחל",
|
||||
"message": "מכין את סביבת העבודה שלך...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "このタグは既に存在します"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "キーボードナビゲーション:",
|
||||
"shortcuts": {
|
||||
"pageUp": "1ページ上にスクロール",
|
||||
"pageDown": "1ページ下にスクロール",
|
||||
"home": "トップにジャンプ",
|
||||
"end": "ボトムにジャンプ"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "初期化中",
|
||||
"message": "ワークスペースを準備中...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "이 태그는 이미 존재합니다"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "키보드 내비게이션:",
|
||||
"shortcuts": {
|
||||
"pageUp": "한 페이지 위로 스크롤",
|
||||
"pageDown": "한 페이지 아래로 스크롤",
|
||||
"home": "맨 위로 이동",
|
||||
"end": "맨 아래로 이동"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "초기화 중",
|
||||
"message": "작업공간을 준비하고 있습니다...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "Этот тег уже существует"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "Навигация с клавиатуры:",
|
||||
"shortcuts": {
|
||||
"pageUp": "Прокрутить на страницу вверх",
|
||||
"pageDown": "Прокрутить на страницу вниз",
|
||||
"home": "Перейти к началу",
|
||||
"end": "Перейти к концу"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "Инициализация",
|
||||
"message": "Подготовка вашего рабочего пространства...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "该标签已存在"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "键盘导航:",
|
||||
"shortcuts": {
|
||||
"pageUp": "向上一页滚动",
|
||||
"pageDown": "向下一页滚动",
|
||||
"home": "跳到顶部",
|
||||
"end": "跳到底部"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "初始化",
|
||||
"message": "正在准备你的工作空间...",
|
||||
|
||||
@@ -1424,15 +1424,6 @@
|
||||
"duplicate": "此標籤已存在"
|
||||
}
|
||||
},
|
||||
"keyboard": {
|
||||
"navigation": "鍵盤導覽:",
|
||||
"shortcuts": {
|
||||
"pageUp": "向上捲動一頁",
|
||||
"pageDown": "向下捲動一頁",
|
||||
"home": "跳至頂部",
|
||||
"end": "跳至底部"
|
||||
}
|
||||
},
|
||||
"initialization": {
|
||||
"title": "初始化",
|
||||
"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
|
||||
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.
|
||||
|
||||
On Windows/macOS where ``posix_fadvise`` is not available the hint is silently
|
||||
skipped.
|
||||
"""
|
||||
sha256_hash = hashlib.sha256()
|
||||
chunk_size = _get_hash_chunk_size_bytes()
|
||||
@@ -48,6 +51,8 @@ async def calculate_sha256(file_path: str) -> str:
|
||||
sha256_hash.update(byte_block)
|
||||
# Evict pages after reading so the data doesn't linger in the kernel page
|
||||
# cache — on WSL this otherwise appears as unreclaimable VmmemWSL growth.
|
||||
# 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()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-lora-manager"
|
||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = [
|
||||
"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-fill: 40%;
|
||||
width: 120px;
|
||||
height: 4px;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: var(--border-color);
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(
|
||||
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;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--lora-accent);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--lora-surface);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.15s ease;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: transform var(--transition-bounce), box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--lora-accent);
|
||||
cursor: pointer;
|
||||
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 {
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 0.85em;
|
||||
font-weight: 700;
|
||||
color: var(--lora-accent);
|
||||
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"] {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
@import 'components/initialization.css';
|
||||
@import 'components/progress-panel.css';
|
||||
@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/sidebar.css'; /* Add sidebar component */
|
||||
@import 'components/media-viewer.css';
|
||||
|
||||
@@ -805,12 +805,14 @@ export class SettingsManager {
|
||||
|
||||
// Set card blur amount slider
|
||||
const cardBlurAmountInput = document.getElementById('cardBlurAmount');
|
||||
const cardBlurValue = state.global.settings.card_blur_amount ?? 8;
|
||||
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');
|
||||
if (cardBlurAmountValue) {
|
||||
cardBlurAmountValue.textContent = `${state.global.settings.card_blur_amount ?? 8}px`;
|
||||
cardBlurAmountValue.textContent = `${cardBlurValue}px`;
|
||||
}
|
||||
|
||||
const usePortableCheckbox = document.getElementById('usePortableSettings');
|
||||
@@ -2070,6 +2072,9 @@ export class SettingsManager {
|
||||
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');
|
||||
} catch (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>
|
||||
</button>
|
||||
</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>
|
||||
@@ -549,7 +549,7 @@
|
||||
</div>
|
||||
<div class="setting-control range-control">
|
||||
<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')">
|
||||
<span id="cardBlurAmountValue" class="range-value">8px</span>
|
||||
</div>
|
||||
|
||||
@@ -137,30 +137,6 @@
|
||||
<span id="doctorStatusBadge" class="doctor-status-badge hidden" aria-hidden="true"></span>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user