mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-13 14:09:25 -03:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
237a015cde | ||
|
|
1ae2778baa | ||
|
|
84fcdb5f20 | ||
|
|
8a0b368b44 | ||
|
|
3990535505 | ||
|
|
3e961a9860 | ||
|
|
d6669f1d04 | ||
|
|
519bafebc8 | ||
|
|
d87863b423 | ||
|
|
84e9fe2dfb | ||
|
|
46cbcf94c8 | ||
|
|
05f3018495 | ||
|
|
f565cc35ca | ||
|
|
dd1cdce16d | ||
|
|
a9e0e7dc8d | ||
|
|
b302d1db7d | ||
|
|
7cbddd9cf7 | ||
|
|
cb8c699224 | ||
|
|
451f74b874 | ||
|
|
a1d248baa6 | ||
|
|
18577fa336 | ||
|
|
5797ce9408 | ||
|
|
826f06255a | ||
|
|
84e16b5c5b | ||
|
|
eb22054580 | ||
|
|
08afb05ece | ||
|
|
f51f125cf1 | ||
|
|
24b2078f21 | ||
|
|
130fb5d2d5 | ||
|
|
23c6863a3a | ||
|
|
c0e2578640 | ||
|
|
e3c812367e | ||
|
|
4d239008a6 | ||
|
|
00177a06d0 | ||
|
|
568daa351e | ||
|
|
5a4664fa12 | ||
|
|
dd5b213adc | ||
|
|
d9ee9b3155 | ||
|
|
01dac57c35 | ||
|
|
7f92d09239 | ||
|
|
62f9e3f44a | ||
|
|
e55895786d | ||
|
|
4e3ede23b7 |
181
.omo/plans/embeddings-hybrid-approach.md
Normal file
181
.omo/plans/embeddings-hybrid-approach.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Embeddings Usage Tracking — Hybrid Approach (Plan C)
|
||||||
|
|
||||||
|
> **Status**: Reference document for future implementation
|
||||||
|
> **Current implementation**: Plan A (prompt text parsing only, see `usage_stats.py:_process_embeddings`)
|
||||||
|
> **Next step**: Add Plan B as a supplement when edge-case coverage is needed
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Embeddings in ComfyUI are not loaded through dedicated ComfyUI nodes like LoRAs or
|
||||||
|
Checkpoints. They are resolved during CLIP tokenization when the prompt text contains
|
||||||
|
`embedding:<name>` syntax (see `comfy/sd1_clip.py:SDTokenizer.tokenize_with_weights`).
|
||||||
|
|
||||||
|
This means the existing metadata_collector hook (which intercepts node execution via
|
||||||
|
`_map_node_over_list`) cannot capture embeddings the same way it captures LoRAs and
|
||||||
|
checkpoints — there is no "EmbeddingLoader" node to intercept.
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
The hybrid approach combines **two complementary mechanisms** to capture embedding
|
||||||
|
usage from all possible paths.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Plan A (已实现) │
|
||||||
|
│ │
|
||||||
|
│ MetadataRegistry.prompt_metadata["prompts"] │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ _process_embeddings() │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Iterate all prompt node texts │
|
||||||
|
│ ├─ regex extract "embedding:<name>" │
|
||||||
|
│ ├─ resolve name → sha256 via EmbeddingScanner │
|
||||||
|
│ └─ UsageStats.stats["embeddings"][sha256]++ │
|
||||||
|
│ │
|
||||||
|
│ Coverage: ~95% — all CLIPTextEncode/Flux/etc nodes │
|
||||||
|
│ │
|
||||||
|
│ Gap: Custom nodes that load embeddings programmatically │
|
||||||
|
│ without putting embedding:name in prompt text │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
+
|
||||||
|
↓ (future: enable Plan B when needed)
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Plan B (未来 — monkey-patch) │
|
||||||
|
│ │
|
||||||
|
│ comfy/sd1_clip.py:load_embed() │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Monkey-patch intercepts EVERY embedding file load │
|
||||||
|
│ │ │
|
||||||
|
│ ├─ Records embedding_name + success/failure │
|
||||||
|
│ ├─ Associates with current prompt_id (via registry)│
|
||||||
|
│ └─ Feeds into UsageStats same as Plan A │
|
||||||
|
│ │
|
||||||
|
│ Coverage: 100% — catches ALL embedding loads │
|
||||||
|
│ │
|
||||||
|
│ Cost: Requires patching into ComfyUI internals │
|
||||||
|
│ (sd1_clip.py, sdxl_clip.py, some text_encoders) │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan B Detail — Monkey-patch `load_embed`
|
||||||
|
|
||||||
|
### Target Function
|
||||||
|
|
||||||
|
**`comfy.sd1_clip.load_embed(embedding_name, embedding_directory, embedding_size, embed_key=None)`**
|
||||||
|
at line 415 of `sd1_clip.py`.
|
||||||
|
|
||||||
|
This is the **single choke point** for all embedding file loads in ComfyUI. Every
|
||||||
|
CLIP variant (SD1, SDXL, SD3, Flux) calls this same function.
|
||||||
|
|
||||||
|
### Implementation Sketch
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In metadata_collector/metadata_hook.py (or a new module)
|
||||||
|
import comfy.sd1_clip as sd1_clip
|
||||||
|
|
||||||
|
_original_load_embed = sd1_clip.load_embed
|
||||||
|
|
||||||
|
def _patched_load_embed(embedding_name, embedding_directory, embedding_size, embed_key=None):
|
||||||
|
result = _original_load_embed(
|
||||||
|
embedding_name, embedding_directory, embedding_size, embed_key
|
||||||
|
)
|
||||||
|
if result is not None:
|
||||||
|
_record_embedding_usage(embedding_name)
|
||||||
|
return result
|
||||||
|
|
||||||
|
sd1_clip.load_embed = _patched_load_embed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prompt ID Association
|
||||||
|
|
||||||
|
The challenge is associating the `load_embed` call with the current `prompt_id`.
|
||||||
|
Options:
|
||||||
|
|
||||||
|
1. **Thread-local / contextvar**: Store current `prompt_id` in a `contextvars.ContextVar`
|
||||||
|
that the metadata_collector sets at the start of each prompt execution.
|
||||||
|
|
||||||
|
2. **MetadataRegistry singleton**: The MetadataRegistry already has `current_prompt_id`.
|
||||||
|
The patch can read it directly since both run in the same thread.
|
||||||
|
|
||||||
|
3. **Lazy aggregation**: Instead of associating with prompt_id at load time, collect
|
||||||
|
all loaded embedding names in a global set during execution, then flush to
|
||||||
|
UsageStats after the prompt completes.
|
||||||
|
|
||||||
|
### Files to Patch
|
||||||
|
|
||||||
|
| File | Function | Coverage |
|
||||||
|
|------|----------|----------|
|
||||||
|
| `comfy/sd1_clip.py:415` | `load_embed()` | Primary — SD1.x, SDXL, SD3, Flux |
|
||||||
|
| `comfy/sdxl_clip.py` | Not needed (calls `sd1_clip.SDTokenizer`) | — |
|
||||||
|
| `comfy/text_encoders/sd3_clip.py` | Not needed (calls `sd1_clip.SDTokenizer`) | — |
|
||||||
|
| `comfy/text_encoders/flux.py` | Not needed (calls `sd1_clip.SDTokenizer`) | — |
|
||||||
|
|
||||||
|
The SD1 tokenizer is the base class for all CLIP variants' tokenizers, so patching
|
||||||
|
`load_embed` covers them all.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
| Edge Case | Plan A | Plan B |
|
||||||
|
|-----------|--------|--------|
|
||||||
|
| `embedding:name` in CLIPTextEncode | ✅ | ✅ |
|
||||||
|
| `embedding:name` in CLIPTextEncodeFlux | ✅ | ✅ |
|
||||||
|
| `embedding:name` in PromptLM (LoRA Manager) | ✅ | ✅ |
|
||||||
|
| `embedding:name` in WAS_Text_to_Conditioning | ✅ | ✅ |
|
||||||
|
| Custom node that loads embedding programmatically | ❌ | ✅ |
|
||||||
|
| Embedding loaded multiple times in same prompt | ✅ (dedup via set) | ✅ (dedup via set) |
|
||||||
|
| Embedding file not found | N/A | ✅ (can log) |
|
||||||
|
| Embedding dimension mismatch | N/A | ✅ (can log) |
|
||||||
|
| Text encoder with non-standard tokenizer (LLaMA, T5...) | Partial | ✅ (if it calls load_embed) |
|
||||||
|
|
||||||
|
## Migration Path: Standalone → Hybrid
|
||||||
|
|
||||||
|
### Phase 1 — Plan A (当前状态)
|
||||||
|
- Prompt text parsing only
|
||||||
|
- No monkey-patching required
|
||||||
|
- Covers all standard workflows
|
||||||
|
|
||||||
|
### Phase 2 — Enable Plan B (未来工作)
|
||||||
|
1. Add monkey-patch of `load_embed` in `metadata_collector/metadata_hook.py` (alongside
|
||||||
|
the existing `_map_node_over_list` hook)
|
||||||
|
2. Collect loaded embedding names in a `set()` on the registry
|
||||||
|
3. In `UsageStats._process_embeddings()`, merge the Plan A results (from prompt text)
|
||||||
|
with the Plan B results (from the patch)
|
||||||
|
4. Add `prompt_data` field on MetadataRegistry to store loaded embeddings per prompt
|
||||||
|
|
||||||
|
### Deduplication
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Merge Plan A + Plan B results in _process_embeddings
|
||||||
|
plan_a_names = extract_from_prompt_texts(prompts_data)
|
||||||
|
plan_b_names = registry.get_loaded_embeddings(prompt_id)
|
||||||
|
|
||||||
|
all_names = plan_a_names | plan_b_names
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Hybrid
|
||||||
|
|
||||||
|
| Scenario | What to verify |
|
||||||
|
|----------|---------------|
|
||||||
|
| Standard `embedding:name` in prompt | Plan A captures it |
|
||||||
|
| Embedding loaded by custom node script | Plan B captures it |
|
||||||
|
| Both paths fire for same embedding | No double-counting (dedup) |
|
||||||
|
| Embedding name resolves to hash | EmbeddingScanner.get_hash_by_filename works |
|
||||||
|
| No embedding scanner available | Graceful skip, no crash |
|
||||||
|
| Missing embedding file | Plan B logs warning, Plan A skips gracefully |
|
||||||
|
| Empty prompt | No crash, no entries |
|
||||||
|
| Standalone mode | Both plans disabled gracefully |
|
||||||
|
|
||||||
|
## Key Files Reference
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `py/utils/usage_stats.py` | Core — `_process_embeddings()` for Plan A |
|
||||||
|
| `py/metadata_collector/constants.py` | `EMBEDDINGS` category constant |
|
||||||
|
| `py/metadata_collector/metadata_hook.py` | Future — monkey-patch for Plan B |
|
||||||
|
| `py/services/embedding_scanner.py` | Hash resolution service |
|
||||||
|
| `py/routes/stats_routes.py` | Already handles `usage_data.get('embeddings', {})` |
|
||||||
|
| `comfy/sd1_clip.py` (ComfyUI) | `load_embed()` — Plan B target |
|
||||||
@@ -6,22 +6,29 @@
|
|||||||
"Scott R"
|
"Scott R"
|
||||||
],
|
],
|
||||||
"allSupporters": [
|
"allSupporters": [
|
||||||
|
"Brennok",
|
||||||
"Insomnia Art Designs",
|
"Insomnia Art Designs",
|
||||||
"2018cfh",
|
"2018cfh",
|
||||||
"megakirbs",
|
"megakirbs",
|
||||||
"Brennok",
|
"Arlecchino Shion",
|
||||||
|
"Rob Williams",
|
||||||
"W+K+White",
|
"W+K+White",
|
||||||
"wackop",
|
"wackop",
|
||||||
"Phil",
|
"Phil",
|
||||||
"Carl G.",
|
"Carl G.",
|
||||||
"Arlecchino Shion",
|
|
||||||
"Charles Blakemore",
|
"Charles Blakemore",
|
||||||
"Rob Williams",
|
|
||||||
"stone9k",
|
"stone9k",
|
||||||
"itismyelement",
|
"itismyelement",
|
||||||
"$MetaSamsara",
|
"$MetaSamsara",
|
||||||
|
"Gingko Biloba",
|
||||||
|
"Kiba",
|
||||||
"onesecondinosaur",
|
"onesecondinosaur",
|
||||||
|
"Christian Byrne",
|
||||||
|
"DM",
|
||||||
|
"Sen314",
|
||||||
|
"Estragon",
|
||||||
"Rosenthal",
|
"Rosenthal",
|
||||||
|
"ClockDaemon",
|
||||||
"Francisco Tatis",
|
"Francisco Tatis",
|
||||||
"Tobi_Swagg",
|
"Tobi_Swagg",
|
||||||
"Andrew Wilson",
|
"Andrew Wilson",
|
||||||
@@ -30,32 +37,38 @@
|
|||||||
"JongWon Han",
|
"JongWon Han",
|
||||||
"VantAI",
|
"VantAI",
|
||||||
"runte3221",
|
"runte3221",
|
||||||
|
"Illrigger",
|
||||||
|
"Tom Corrigan",
|
||||||
"FreelancerZ",
|
"FreelancerZ",
|
||||||
|
"Echo",
|
||||||
|
"Robert Stacey",
|
||||||
"Edgar Tejeda",
|
"Edgar Tejeda",
|
||||||
"Fraser Cross",
|
"Fraser Cross",
|
||||||
"Liam MacDougal",
|
"Liam MacDougal",
|
||||||
"Polymorphic Indeterminate",
|
"Polymorphic Indeterminate",
|
||||||
|
"Sterilized",
|
||||||
|
"JORGE+LUIZ+HUSSNI+MESSIAS",
|
||||||
"Marc Whiffen",
|
"Marc Whiffen",
|
||||||
"Skalabananen",
|
"Skalabananen",
|
||||||
"Birdy",
|
"Birdy",
|
||||||
"Kiba",
|
|
||||||
"Mozzel",
|
"Mozzel",
|
||||||
"Gingko Biloba",
|
|
||||||
"Reno Lam",
|
"Reno Lam",
|
||||||
|
"JSST",
|
||||||
"sig",
|
"sig",
|
||||||
"Christian Byrne",
|
|
||||||
"DM",
|
|
||||||
"Sen314",
|
|
||||||
"Estragon",
|
|
||||||
"J\\B/ 8r0wns0n",
|
"J\\B/ 8r0wns0n",
|
||||||
"Snaggwort",
|
"Snaggwort",
|
||||||
"Takkan",
|
"Takkan",
|
||||||
"Matt+J",
|
"Matt+J",
|
||||||
"ClockDaemon",
|
"Baekdoosixt",
|
||||||
|
"Jonathan Ross",
|
||||||
"KD",
|
"KD",
|
||||||
"Omnidex",
|
"Omnidex",
|
||||||
|
"Nazono_hito",
|
||||||
|
"daniel dove",
|
||||||
"Tyler Trebuchon",
|
"Tyler Trebuchon",
|
||||||
"Release Cabrakan",
|
"Release Cabrakan",
|
||||||
|
"JW Sin",
|
||||||
|
"Alex",
|
||||||
"SG",
|
"SG",
|
||||||
"carozzz",
|
"carozzz",
|
||||||
"James Dooley",
|
"James Dooley",
|
||||||
@@ -70,77 +83,71 @@
|
|||||||
"RedrockVP",
|
"RedrockVP",
|
||||||
"Wolffen",
|
"Wolffen",
|
||||||
"James Todd",
|
"James Todd",
|
||||||
|
"Wicked Choices by ASLPro3D",
|
||||||
"Steven Pfeiffer",
|
"Steven Pfeiffer",
|
||||||
"Tim",
|
"レプサイ",
|
||||||
"Timmy",
|
"Timmy",
|
||||||
"Johnny",
|
"Johnny",
|
||||||
|
"Tak",
|
||||||
"Lisster",
|
"Lisster",
|
||||||
"Michael Wong",
|
"Michael Wong",
|
||||||
"Illrigger",
|
"Big Red",
|
||||||
"whudunit",
|
"whudunit",
|
||||||
"Tom Corrigan",
|
"dl0901dm",
|
||||||
"JackieWang",
|
"JackieWang",
|
||||||
"fnkylove",
|
"fnkylove",
|
||||||
"Yushio",
|
"Yushio",
|
||||||
"Vik71it",
|
"Vik71it",
|
||||||
"Echo",
|
"Bishoujoker",
|
||||||
"Lilleman",
|
"Lilleman",
|
||||||
"Robert Stacey",
|
|
||||||
"PM",
|
"PM",
|
||||||
"Todd Keck",
|
"Todd Keck",
|
||||||
"Briton Heilbrun",
|
"Briton Heilbrun",
|
||||||
"Jorge Hussni",
|
"wildnut",
|
||||||
"Sterilized",
|
"Aleksander Wujczyk",
|
||||||
|
"AM Kuro",
|
||||||
"BadassArabianMofo",
|
"BadassArabianMofo",
|
||||||
"Pascal Dahle",
|
"Pascal Dahle",
|
||||||
"quarz",
|
"quarz",
|
||||||
"Greg",
|
"Greg",
|
||||||
"JSST",
|
|
||||||
"lmsupporter",
|
"lmsupporter",
|
||||||
|
"andrew.tappan",
|
||||||
"zounic",
|
"zounic",
|
||||||
"wfpearl",
|
"wfpearl",
|
||||||
"Baekdoosixt",
|
|
||||||
"Jonathan Ross",
|
|
||||||
"Jack B Nimble",
|
"Jack B Nimble",
|
||||||
"Nazono_hito",
|
|
||||||
"Melville Parrish",
|
"Melville Parrish",
|
||||||
"daniel dove",
|
|
||||||
"Lustre",
|
"Lustre",
|
||||||
"JW Sin",
|
"JaxMax",
|
||||||
"contrite831",
|
"contrite831",
|
||||||
"Alex",
|
|
||||||
"bh",
|
"bh",
|
||||||
"Marlon Daniels",
|
"Marlon Daniels",
|
||||||
"Starkselle",
|
"Starkselle",
|
||||||
"Aaron Bleuer",
|
"Aaron Bleuer",
|
||||||
"LacesOut!",
|
"LacesOut!",
|
||||||
"greebles",
|
"greebles",
|
||||||
|
"Some Guy Named Barry",
|
||||||
"M Postkasse",
|
"M Postkasse",
|
||||||
"Gooohokrbe",
|
"Gooohokrbe",
|
||||||
"Wicked Choices by ASLPro3D",
|
|
||||||
"OldBones",
|
"OldBones",
|
||||||
"Jacob Hoehler",
|
"Jacob Hoehler",
|
||||||
"FinalyFree",
|
"FinalyFree",
|
||||||
|
"Matt Wenzel",
|
||||||
"Weasyl",
|
"Weasyl",
|
||||||
"Lex Song",
|
"Lex Song",
|
||||||
"Cory Paza",
|
"Cory Paza",
|
||||||
"Tak",
|
|
||||||
"Gonzalo Andre Allendes Lopez",
|
"Gonzalo Andre Allendes Lopez",
|
||||||
"Zach Gonser",
|
"Zach Gonser",
|
||||||
"Big Red",
|
|
||||||
"Jimmy Ledbetter",
|
"Jimmy Ledbetter",
|
||||||
"Luc Job",
|
"Luc Job",
|
||||||
"dl0901dm",
|
|
||||||
"Philip Hempel",
|
"Philip Hempel",
|
||||||
"corde",
|
"corde",
|
||||||
"Nick Walker",
|
"Nick Walker",
|
||||||
"Bishoujoker",
|
"dan",
|
||||||
"aai",
|
"aai",
|
||||||
"Tori",
|
"Tori",
|
||||||
"wildnut",
|
"otaku fra",
|
||||||
"jean jahren",
|
"jean jahren",
|
||||||
"Aleksander Wujczyk",
|
"MiraiKuriyamaSy",
|
||||||
"AM Kuro",
|
|
||||||
"Ran C",
|
"Ran C",
|
||||||
"ViperC",
|
"ViperC",
|
||||||
"Penfore",
|
"Penfore",
|
||||||
@@ -149,17 +156,22 @@
|
|||||||
"Karl P.",
|
"Karl P.",
|
||||||
"Akira_HentAI",
|
"Akira_HentAI",
|
||||||
"Gordon Cole",
|
"Gordon Cole",
|
||||||
|
"Adam Taylor",
|
||||||
"AbstractAss",
|
"AbstractAss",
|
||||||
"andrew.tappan",
|
"Weird_With_A_Beard",
|
||||||
"N/A",
|
"N/A",
|
||||||
"The Spawn",
|
"The Spawn",
|
||||||
"graysock",
|
"graysock",
|
||||||
|
"Pozadine1",
|
||||||
|
"Qarob",
|
||||||
|
"AIGooner",
|
||||||
|
"Luc",
|
||||||
"Greenmoustache",
|
"Greenmoustache",
|
||||||
|
"Jackthemind",
|
||||||
"fancypants",
|
"fancypants",
|
||||||
"Eldithor",
|
"Eldithor",
|
||||||
"Joboshy",
|
"Joboshy",
|
||||||
"Digital",
|
"Digital",
|
||||||
"JaxMax",
|
|
||||||
"takyamtom",
|
"takyamtom",
|
||||||
"Bohemian Corporal",
|
"Bohemian Corporal",
|
||||||
"Dan",
|
"Dan",
|
||||||
@@ -170,42 +182,37 @@
|
|||||||
"carey6409",
|
"carey6409",
|
||||||
"Olive",
|
"Olive",
|
||||||
"太郎 ゲーム",
|
"太郎 ゲーム",
|
||||||
"Some Guy Named Barry",
|
"Roslynd",
|
||||||
"jinxedx",
|
"jinxedx",
|
||||||
"Cosmosis",
|
"Cosmosis",
|
||||||
"AELOX",
|
"AELOX",
|
||||||
"Dankin",
|
"Dankin",
|
||||||
"Nicfit23",
|
"Nicfit23",
|
||||||
"FloPro4Sho",
|
"FloPro4Sho",
|
||||||
|
"Cristian Vazquez",
|
||||||
"wamekukyouzin",
|
"wamekukyouzin",
|
||||||
"drum matthieu",
|
"drum matthieu",
|
||||||
"Dogmaster",
|
"Dogmaster",
|
||||||
"Matt Wenzel",
|
|
||||||
"Frank Nitty",
|
"Frank Nitty",
|
||||||
|
"Magic Noob",
|
||||||
"Christopher Michel",
|
"Christopher Michel",
|
||||||
"Serge Bekenkamp",
|
"Serge Bekenkamp",
|
||||||
|
"DougPeterson",
|
||||||
"LeoZero",
|
"LeoZero",
|
||||||
"Antonio Pontes",
|
"Antonio Pontes",
|
||||||
"ApathyJones",
|
"ApathyJones",
|
||||||
"Julian V",
|
"Julian V",
|
||||||
"Steven Owens",
|
"Steven Owens",
|
||||||
"nahinahi9",
|
"nahinahi9",
|
||||||
|
"Kevin John Duck",
|
||||||
"Dustin Chen",
|
"Dustin Chen",
|
||||||
"dan",
|
|
||||||
"Blackfish95",
|
"Blackfish95",
|
||||||
"Mouthlessman",
|
"Mouthlessman",
|
||||||
"Paul Kroll",
|
"Paul Kroll",
|
||||||
"otaku fra",
|
|
||||||
"MiraiKuriyamaSy",
|
|
||||||
"Bas Imagineer",
|
"Bas Imagineer",
|
||||||
"yuxz69",
|
"yuxz69",
|
||||||
"Adam Taylor",
|
|
||||||
"Weird_With_A_Beard",
|
|
||||||
"esthe",
|
"esthe",
|
||||||
"Pozadine1",
|
"decoy",
|
||||||
"Qarob",
|
|
||||||
"AIGooner",
|
|
||||||
"Luc",
|
|
||||||
"ProtonPrince",
|
"ProtonPrince",
|
||||||
"DiffDuck",
|
"DiffDuck",
|
||||||
"elu3199",
|
"elu3199",
|
||||||
@@ -217,46 +224,50 @@
|
|||||||
"wundershark",
|
"wundershark",
|
||||||
"mr_dinosaur",
|
"mr_dinosaur",
|
||||||
"Tyrswood",
|
"Tyrswood",
|
||||||
|
"Ray Wing",
|
||||||
|
"Ranzitho",
|
||||||
|
"Gus",
|
||||||
|
"MJG",
|
||||||
"linnfrey",
|
"linnfrey",
|
||||||
"IamAyam",
|
"IamAyam",
|
||||||
"skaterb949",
|
"skaterb949",
|
||||||
"Josef Lanzl",
|
"Josef Lanzl",
|
||||||
|
"Nerezza",
|
||||||
"confiscated Zyra",
|
"confiscated Zyra",
|
||||||
"Error_Rule34_Not_found",
|
"Error_Rule34_Not_found",
|
||||||
|
"aezin",
|
||||||
|
"jcay015",
|
||||||
"Gerald Welly",
|
"Gerald Welly",
|
||||||
"Roslynd",
|
"Erik Lopez",
|
||||||
|
"Mateo Curić",
|
||||||
"Tee Gee",
|
"Tee Gee",
|
||||||
"Geolog",
|
"Geolog",
|
||||||
"tarek helmi",
|
"tarek helmi",
|
||||||
"Neco28",
|
"Neco28",
|
||||||
|
"Eris3D",
|
||||||
"Max Marklund",
|
"Max Marklund",
|
||||||
"David Ortega",
|
"David Ortega",
|
||||||
"Cristian Vazquez",
|
|
||||||
"Magic Noob",
|
|
||||||
"Pronredn",
|
"Pronredn",
|
||||||
"DougPeterson",
|
"a _",
|
||||||
"Jeff",
|
"Jeff",
|
||||||
"Bruce",
|
"Bruce",
|
||||||
"lh qwe",
|
"lh qwe",
|
||||||
"Kevin John Duck",
|
"James Coleman",
|
||||||
"conner",
|
"conner",
|
||||||
"Kevin Christopher",
|
"Kevin Christopher",
|
||||||
|
"Chad Idk",
|
||||||
"dd",
|
"dd",
|
||||||
"Princess Bright Eyes",
|
"Princess Bright Eyes",
|
||||||
"Dušan Ryban",
|
"Dušan Ryban",
|
||||||
"Felipe dos Santos",
|
"Felipe dos Santos",
|
||||||
|
"sjon kreutz",
|
||||||
"John Statham",
|
"John Statham",
|
||||||
"Douglas Gaspar",
|
"Douglas Gaspar",
|
||||||
"Metryman55",
|
"Metryman55",
|
||||||
"AlexDuKaNa",
|
"AlexDuKaNa",
|
||||||
"George",
|
"George",
|
||||||
"dw",
|
"dw",
|
||||||
"decoy",
|
|
||||||
"Ray Wing",
|
|
||||||
"Ranzitho",
|
|
||||||
"Gus",
|
|
||||||
"地獄の禄",
|
"地獄の禄",
|
||||||
"MJG",
|
|
||||||
"David LaVallee",
|
"David LaVallee",
|
||||||
"ae",
|
"ae",
|
||||||
"Tr4shP4nda",
|
"Tr4shP4nda",
|
||||||
@@ -273,19 +284,20 @@
|
|||||||
"몽타주",
|
"몽타주",
|
||||||
"Kland",
|
"Kland",
|
||||||
"Hailshem",
|
"Hailshem",
|
||||||
|
"kudari",
|
||||||
|
"Naomi Hale Danchi",
|
||||||
|
"epicgamer0020690",
|
||||||
|
"Richard",
|
||||||
"奚明 刘",
|
"奚明 刘",
|
||||||
|
"Andrew",
|
||||||
"Brian M",
|
"Brian M",
|
||||||
"Nerezza",
|
"Robert Wegemund",
|
||||||
"sanborondon",
|
"sanborondon",
|
||||||
"준희 김",
|
"준희 김",
|
||||||
"Taylor Funk",
|
"Taylor Funk",
|
||||||
"aezin",
|
|
||||||
"Thought2Form",
|
"Thought2Form",
|
||||||
"jcay015",
|
|
||||||
"Kevin Picco",
|
"Kevin Picco",
|
||||||
"Erik Lopez",
|
"Sadlip",
|
||||||
"Mateo Curić",
|
|
||||||
"Eris3D",
|
|
||||||
"Tomohiro Baba",
|
"Tomohiro Baba",
|
||||||
"m",
|
"m",
|
||||||
"Noora",
|
"Noora",
|
||||||
@@ -294,32 +306,30 @@
|
|||||||
"Mattssn",
|
"Mattssn",
|
||||||
"Mikko Hemilä",
|
"Mikko Hemilä",
|
||||||
"Jamie Ogletree",
|
"Jamie Ogletree",
|
||||||
"a _",
|
"Michael Taylor",
|
||||||
"James Coleman",
|
|
||||||
"Martial",
|
"Martial",
|
||||||
"Emil Andersson",
|
"Emil Andersson",
|
||||||
"Ouro Boros",
|
"Ouro Boros",
|
||||||
"Chad Idk",
|
"Atilla Berke Pekduyar",
|
||||||
"Steam Steam",
|
"Steam Steam",
|
||||||
"CryptoTraderJK",
|
"CryptoTraderJK",
|
||||||
|
"Decx _",
|
||||||
"Yuji Kaneko",
|
"Yuji Kaneko",
|
||||||
"Davaitamin",
|
"Davaitamin",
|
||||||
"Rops Alot",
|
"Rops Alot",
|
||||||
"tedcor",
|
"tedcor",
|
||||||
"Sam",
|
"Sam",
|
||||||
"Fotek Design",
|
"Fotek Design",
|
||||||
"sjon kreutz",
|
|
||||||
"Ace Ventura",
|
"Ace Ventura",
|
||||||
|
"LarsesFPC",
|
||||||
"MadSpin",
|
"MadSpin",
|
||||||
"inbijiburu",
|
"inbijiburu",
|
||||||
"Nick “Loadstone” D",
|
"Nick “Loadstone” D",
|
||||||
"momokai",
|
"momokai",
|
||||||
"starbugx",
|
"starbugx",
|
||||||
"kudari",
|
|
||||||
"Naomi Hale Danchi",
|
|
||||||
"dc7431",
|
"dc7431",
|
||||||
"ken",
|
"ken",
|
||||||
"epicgamer0020690",
|
"Crocket",
|
||||||
"Joshua Porrata",
|
"Joshua Porrata",
|
||||||
"keemun",
|
"keemun",
|
||||||
"SuBu",
|
"SuBu",
|
||||||
@@ -339,22 +349,24 @@
|
|||||||
"KitKatM",
|
"KitKatM",
|
||||||
"socrasteeze",
|
"socrasteeze",
|
||||||
"OrganicArtifact",
|
"OrganicArtifact",
|
||||||
|
"MudkipMedkitz",
|
||||||
|
"deanbrian",
|
||||||
|
"Alex Wortman",
|
||||||
|
"Cody",
|
||||||
|
"emadsultan",
|
||||||
"Vir",
|
"Vir",
|
||||||
"gzmzmvp",
|
"gzmzmvp",
|
||||||
"Richard",
|
|
||||||
"Andrew",
|
|
||||||
"Robert Wegemund",
|
|
||||||
"Littlehuggy",
|
"Littlehuggy",
|
||||||
"Gregory Kozhemiak",
|
"Gregory Kozhemiak",
|
||||||
"Draven T",
|
"Draven T",
|
||||||
"mrjuan",
|
"mrjuan",
|
||||||
"Brian Buie",
|
"Brian Buie",
|
||||||
"Sadlip",
|
|
||||||
"Eric Whitney",
|
"Eric Whitney",
|
||||||
"Joey Callahan",
|
"Joey Callahan",
|
||||||
"Aquatic Coffee",
|
"Aquatic Coffee",
|
||||||
"Ivan Tadic",
|
"Ivan Tadic",
|
||||||
"Mike Simone",
|
"Mike Simone",
|
||||||
|
"John J Linehan",
|
||||||
"ethanfel",
|
"ethanfel",
|
||||||
"Elliot E",
|
"Elliot E",
|
||||||
"Morgandel",
|
"Morgandel",
|
||||||
@@ -366,34 +378,30 @@
|
|||||||
"Sloan Steddy",
|
"Sloan Steddy",
|
||||||
"Temikus",
|
"Temikus",
|
||||||
"Artokun",
|
"Artokun",
|
||||||
"Michael Taylor",
|
"hexxish",
|
||||||
"Derek Baker",
|
"Derek Baker",
|
||||||
"Anthony Faxlandez",
|
"Anthony Faxlandez",
|
||||||
"battu",
|
"battu",
|
||||||
"Michael Anthony Scott",
|
"Michael Anthony Scott",
|
||||||
"Atilla Berke Pekduyar",
|
|
||||||
"Nathan",
|
"Nathan",
|
||||||
"Decx _",
|
"NICHOLAS BAXLEY",
|
||||||
"Pat Hen",
|
"Pat Hen",
|
||||||
|
"Xeeosat",
|
||||||
|
"Ed Wang",
|
||||||
"Jordan Shaw",
|
"Jordan Shaw",
|
||||||
|
"g unit",
|
||||||
"Srdb",
|
"Srdb",
|
||||||
"四糸凜音",
|
"四糸凜音",
|
||||||
"Nihongasuki",
|
"Nihongasuki",
|
||||||
"LarsesFPC",
|
|
||||||
"JC",
|
"JC",
|
||||||
"Prompt Pirate",
|
"Prompt Pirate",
|
||||||
"uwutismxd",
|
"uwutismxd",
|
||||||
"FrxzenSnxw",
|
"FrxzenSnxw",
|
||||||
"zenobeus",
|
"zenobeus",
|
||||||
"Crocket",
|
|
||||||
"Jackthemind",
|
|
||||||
"ryoma",
|
"ryoma",
|
||||||
"Stryker",
|
"Stryker",
|
||||||
"ResidentDeviant",
|
"ResidentDeviant",
|
||||||
"MudkipMedkitz",
|
"Ginnie",
|
||||||
"deanbrian",
|
|
||||||
"Alex Wortman",
|
|
||||||
"Cody",
|
|
||||||
"Raku",
|
"Raku",
|
||||||
"smart.edge5178",
|
"smart.edge5178",
|
||||||
"InformedViewz",
|
"InformedViewz",
|
||||||
@@ -415,6 +423,15 @@
|
|||||||
"SpringBootisTrash",
|
"SpringBootisTrash",
|
||||||
"carsten",
|
"carsten",
|
||||||
"ikok",
|
"ikok",
|
||||||
|
"DarkRoast",
|
||||||
|
"letzte",
|
||||||
|
"Nasty+Hobbit",
|
||||||
|
"Sora+Yori",
|
||||||
|
"lrdchs2",
|
||||||
|
"Duk3+Rand0m",
|
||||||
|
"Nathen+Choi",
|
||||||
|
"T",
|
||||||
|
"cocona",
|
||||||
"ElitaSSJ4",
|
"ElitaSSJ4",
|
||||||
"David Schenck",
|
"David Schenck",
|
||||||
"Wolfe7D1",
|
"Wolfe7D1",
|
||||||
@@ -426,7 +443,6 @@
|
|||||||
"Goldwaters",
|
"Goldwaters",
|
||||||
"Kauffy",
|
"Kauffy",
|
||||||
"Zude",
|
"Zude",
|
||||||
"John J Linehan",
|
|
||||||
"Kyler",
|
"Kyler",
|
||||||
"Edward Kennedy",
|
"Edward Kennedy",
|
||||||
"Justin Blaylock",
|
"Justin Blaylock",
|
||||||
@@ -435,17 +451,14 @@
|
|||||||
"Vane Holzer",
|
"Vane Holzer",
|
||||||
"psytrax",
|
"psytrax",
|
||||||
"Cyrus Fett",
|
"Cyrus Fett",
|
||||||
"hexxish",
|
"Xenon Xue",
|
||||||
"notedfakes",
|
"notedfakes",
|
||||||
"Billy Gladky",
|
"Billy Gladky",
|
||||||
"NICHOLAS BAXLEY",
|
|
||||||
"Michael Scott",
|
"Michael Scott",
|
||||||
"Probis",
|
"Probis",
|
||||||
"Ed Wang",
|
|
||||||
"Wes Sims",
|
"Wes Sims",
|
||||||
"ItsGeneralButtNaked",
|
"ItsGeneralButtNaked",
|
||||||
"Donor4115",
|
"Donor4115",
|
||||||
"g unit",
|
|
||||||
"Distortik",
|
"Distortik",
|
||||||
"Filippo Ferrari",
|
"Filippo Ferrari",
|
||||||
"Youguang",
|
"Youguang",
|
||||||
@@ -460,9 +473,14 @@
|
|||||||
"Mitchell Robson",
|
"Mitchell Robson",
|
||||||
"Whitepinetrader",
|
"Whitepinetrader",
|
||||||
"POPPIN",
|
"POPPIN",
|
||||||
"Ginnie",
|
|
||||||
"emadsultan",
|
|
||||||
"nanana",
|
"nanana",
|
||||||
|
"ChaChanoKo",
|
||||||
|
"ghoulars",
|
||||||
|
"null",
|
||||||
|
"Beau",
|
||||||
|
"redcarrot",
|
||||||
|
"powerbot99",
|
||||||
|
"Fthehappy",
|
||||||
"g",
|
"g",
|
||||||
"J",
|
"J",
|
||||||
"Alan+Cano",
|
"Alan+Cano",
|
||||||
@@ -474,15 +492,6 @@
|
|||||||
"quantenmecha",
|
"quantenmecha",
|
||||||
"Jason+Nash",
|
"Jason+Nash",
|
||||||
"BillyBoy84",
|
"BillyBoy84",
|
||||||
"DarkRoast",
|
|
||||||
"letzte",
|
|
||||||
"Nasty+Hobbit",
|
|
||||||
"Sora+Yori",
|
|
||||||
"lrdchs2",
|
|
||||||
"Duk3+Rand0m",
|
|
||||||
"Nathen+Choi",
|
|
||||||
"T",
|
|
||||||
"cocona",
|
|
||||||
"Buecyb99",
|
"Buecyb99",
|
||||||
"Welkor",
|
"Welkor",
|
||||||
"John Martin",
|
"John Martin",
|
||||||
@@ -491,6 +500,8 @@
|
|||||||
"moranqianlong",
|
"moranqianlong",
|
||||||
"Kalli Core",
|
"Kalli Core",
|
||||||
"Time Valentine",
|
"Time Valentine",
|
||||||
|
"Christian Schäfer",
|
||||||
|
"りん あめ",
|
||||||
"Михал Михалыч",
|
"Михал Михалыч",
|
||||||
"Matt",
|
"Matt",
|
||||||
"Frogmilk",
|
"Frogmilk",
|
||||||
@@ -501,21 +512,26 @@
|
|||||||
"Anonym dkjglfleeoeldldldlkf",
|
"Anonym dkjglfleeoeldldldlkf",
|
||||||
"Ezokewn",
|
"Ezokewn",
|
||||||
"SendingRavens",
|
"SendingRavens",
|
||||||
"Xenon Xue",
|
|
||||||
"JackJohnnyJim",
|
"JackJohnnyJim",
|
||||||
|
"TenaciousD",
|
||||||
|
"Dmitry Ryzhov",
|
||||||
|
"Khánh Đặng",
|
||||||
"Edward Ten Eyck",
|
"Edward Ten Eyck",
|
||||||
"Michael Docherty",
|
"Michael Docherty",
|
||||||
|
"Jimmy Borup",
|
||||||
"Paul Hartsuyker",
|
"Paul Hartsuyker",
|
||||||
"Henrique Faiolli",
|
|
||||||
"elitassj",
|
"elitassj",
|
||||||
"Solixer",
|
"Solixer",
|
||||||
|
"Pete Pain",
|
||||||
"Jacob Winter",
|
"Jacob Winter",
|
||||||
"Ryan Presley Ng",
|
"Ryan Presley Ng",
|
||||||
"jinksta187",
|
"jinksta187",
|
||||||
|
"RHopkirk",
|
||||||
"Andrew Wilkinson",
|
"Andrew Wilkinson",
|
||||||
"Manu Thetug",
|
"Manu Thetug",
|
||||||
"Karlanx",
|
"Karlanx",
|
||||||
"Lyavph",
|
"Lyavph",
|
||||||
|
"Maxim",
|
||||||
"David",
|
"David",
|
||||||
"Meilo",
|
"Meilo",
|
||||||
"operationancut",
|
"operationancut",
|
||||||
@@ -537,6 +553,17 @@
|
|||||||
"Scott",
|
"Scott",
|
||||||
"Muratoraccio",
|
"Muratoraccio",
|
||||||
"D",
|
"D",
|
||||||
|
"2turbo",
|
||||||
|
"Somebody",
|
||||||
|
"Balut+Omelette",
|
||||||
|
"Dmitry+Viznesenskiy",
|
||||||
|
"tanjin90",
|
||||||
|
"sternenkrieger",
|
||||||
|
"eriick",
|
||||||
|
"Patrick+Bryan",
|
||||||
|
"Pascalou",
|
||||||
|
"lighthawke",
|
||||||
|
"Lev+Lanevskiy",
|
||||||
"low9",
|
"low9",
|
||||||
"Winged",
|
"Winged",
|
||||||
"YassineKhaled",
|
"YassineKhaled",
|
||||||
@@ -552,13 +579,6 @@
|
|||||||
"Alex",
|
"Alex",
|
||||||
"Jacky+Ho",
|
"Jacky+Ho",
|
||||||
"Karru",
|
"Karru",
|
||||||
"ghoulars",
|
|
||||||
"ChaChanoKo",
|
|
||||||
"null",
|
|
||||||
"Beau",
|
|
||||||
"redcarrot",
|
|
||||||
"powerbot99",
|
|
||||||
"Fthehappy",
|
|
||||||
"generic404",
|
"generic404",
|
||||||
"abattoirblues",
|
"abattoirblues",
|
||||||
"zounik",
|
"zounik",
|
||||||
@@ -568,9 +588,10 @@
|
|||||||
"Bob Barker",
|
"Bob Barker",
|
||||||
"edk",
|
"edk",
|
||||||
"Tú Nguyễn Lý Hoàng",
|
"Tú Nguyễn Lý Hoàng",
|
||||||
|
"shira1011",
|
||||||
|
"Ben D",
|
||||||
|
"G",
|
||||||
"Ronan Delevacq",
|
"Ronan Delevacq",
|
||||||
"Christian Schäfer",
|
|
||||||
"りん あめ",
|
|
||||||
"ja s",
|
"ja s",
|
||||||
"Doug Mason",
|
"Doug Mason",
|
||||||
"Jeremy Townsend",
|
"Jeremy Townsend",
|
||||||
@@ -580,38 +601,41 @@
|
|||||||
"Sean voets",
|
"Sean voets",
|
||||||
"Owen Gwosdz",
|
"Owen Gwosdz",
|
||||||
"Jarrid Lee",
|
"Jarrid Lee",
|
||||||
|
"Poophead27 Blyat",
|
||||||
"Kor",
|
"Kor",
|
||||||
"Joseph Hanson",
|
"Joseph Hanson",
|
||||||
"John Rednoulf",
|
"John Rednoulf",
|
||||||
|
"Spire",
|
||||||
"Boba Smith",
|
"Boba Smith",
|
||||||
"Devil Lude",
|
"Devil Lude",
|
||||||
"David Murcko",
|
"David Murcko",
|
||||||
"MR.Bear",
|
"MR.Bear",
|
||||||
"Jack Dole",
|
"Jack Dole",
|
||||||
|
"somethingtosay8",
|
||||||
|
"ivistorm",
|
||||||
"max blo",
|
"max blo",
|
||||||
"Sauv",
|
"Sauv",
|
||||||
"Steven",
|
"Steven",
|
||||||
"CptNeo",
|
"CptNeo",
|
||||||
"TenaciousD",
|
|
||||||
"Dmitry Ryzhov",
|
|
||||||
"Khánh Đặng",
|
|
||||||
"Maso",
|
"Maso",
|
||||||
|
"Ted Cart",
|
||||||
|
"Sage Himeros",
|
||||||
"Eric Ketchum",
|
"Eric Ketchum",
|
||||||
"Kevin Wallace",
|
"Kevin Wallace",
|
||||||
"Jimmy Borup",
|
"David Spearing",
|
||||||
"ChicRic",
|
"ChicRic",
|
||||||
"Tigon",
|
"Tigon",
|
||||||
"BastardSama",
|
"BastardSama",
|
||||||
"mercur",
|
"mercur",
|
||||||
"Pete Pain",
|
"Tania Nayelli Fernandez",
|
||||||
"RHopkirk",
|
"Draconach",
|
||||||
"Yavizu3d",
|
"Yavizu3d",
|
||||||
"Maxim",
|
|
||||||
"Yves Poezevara",
|
"Yves Poezevara",
|
||||||
"Teriak47",
|
"Teriak47",
|
||||||
"Just me",
|
"Just me",
|
||||||
"Raf Stahelin",
|
"Raf Stahelin",
|
||||||
"Вячеслав Маринин",
|
"Вячеслав Маринин",
|
||||||
|
"Dkommander22",
|
||||||
"Cola Matthew",
|
"Cola Matthew",
|
||||||
"OniNoKen",
|
"OniNoKen",
|
||||||
"Iain Wisely",
|
"Iain Wisely",
|
||||||
@@ -655,6 +679,17 @@
|
|||||||
"SelfishMedic",
|
"SelfishMedic",
|
||||||
"adderleighn",
|
"adderleighn",
|
||||||
"EnragedAntelope",
|
"EnragedAntelope",
|
||||||
|
"Monix",
|
||||||
|
"Trolinka",
|
||||||
|
"IshouI;_;",
|
||||||
|
"PredragR",
|
||||||
|
"Clauzmak",
|
||||||
|
"Nerick",
|
||||||
|
"JoL",
|
||||||
|
"Gold_miner_ego",
|
||||||
|
"SundayRage",
|
||||||
|
"YoruHime",
|
||||||
|
"matter",
|
||||||
"SRCRCOSS",
|
"SRCRCOSS",
|
||||||
"imer",
|
"imer",
|
||||||
"Akkas+Haque",
|
"Akkas+Haque",
|
||||||
@@ -675,18 +710,8 @@
|
|||||||
"Sildoren",
|
"Sildoren",
|
||||||
"Darvidous",
|
"Darvidous",
|
||||||
"Seon+Song",
|
"Seon+Song",
|
||||||
"2turbo",
|
|
||||||
"balut+omelette",
|
|
||||||
"Nebuleux",
|
"Nebuleux",
|
||||||
"Dmitry+Viznesenskiy",
|
|
||||||
"Tanjin90",
|
|
||||||
"Somebody",
|
|
||||||
"sternenkrieger",
|
|
||||||
"eriick",
|
|
||||||
"Join+Chun",
|
"Join+Chun",
|
||||||
"Pascalou",
|
|
||||||
"lighthawke",
|
|
||||||
"Terraformer",
|
|
||||||
"GDS+DEV",
|
"GDS+DEV",
|
||||||
"4rt+r3d",
|
"4rt+r3d",
|
||||||
"you+halo9",
|
"you+halo9",
|
||||||
@@ -712,17 +737,16 @@
|
|||||||
"_ G3n",
|
"_ G3n",
|
||||||
"Donovan Jenkins",
|
"Donovan Jenkins",
|
||||||
"Hans Meier",
|
"Hans Meier",
|
||||||
"shira1011",
|
|
||||||
"sicarius",
|
"sicarius",
|
||||||
"Michael Eid",
|
"Michael Eid",
|
||||||
|
"Wolf and Fox Legends",
|
||||||
"beersandbacon",
|
"beersandbacon",
|
||||||
"Neko Desco",
|
"Neko Desco",
|
||||||
"Bob barker",
|
"Bob barker",
|
||||||
"Ben D",
|
|
||||||
"Ninja Tom",
|
"Ninja Tom",
|
||||||
"G",
|
|
||||||
"karim ben brik",
|
"karim ben brik",
|
||||||
"Vinarus",
|
"Vinarus",
|
||||||
|
"Josh Snyder",
|
||||||
"Michael Zhu",
|
"Michael Zhu",
|
||||||
"Nemisu",
|
"Nemisu",
|
||||||
"Seraphy",
|
"Seraphy",
|
||||||
@@ -732,41 +756,42 @@
|
|||||||
"jumpd",
|
"jumpd",
|
||||||
"John C",
|
"John C",
|
||||||
"Rim",
|
"Rim",
|
||||||
|
"Room Light",
|
||||||
"Jairus Knudsen",
|
"Jairus Knudsen",
|
||||||
"Poophead27 Blyat",
|
|
||||||
"Xan Dionysus",
|
"Xan Dionysus",
|
||||||
|
"Patryk Serious",
|
||||||
"Nathan lee",
|
"Nathan lee",
|
||||||
"Lyle Liston",
|
"lylepaul",
|
||||||
"Middo",
|
"Middo",
|
||||||
"Forbidden Atelier",
|
"Forbidden Atelier",
|
||||||
"Thomas Sankowski",
|
"Thomas Sankowski",
|
||||||
"Spire",
|
|
||||||
"DrB",
|
"DrB",
|
||||||
"AZ Party Oasis",
|
"AZ Party Oasis",
|
||||||
"Adictedtohumping",
|
"Adictedtohumping",
|
||||||
|
"Snorklebort",
|
||||||
"Towelie",
|
"Towelie",
|
||||||
"TheFusion",
|
"TheFusion",
|
||||||
"matt",
|
"matt",
|
||||||
"dsffsdfsdfsdfsdfsdf",
|
"dsffsdfsdfsdfsdfsdf",
|
||||||
"somethingtosay8",
|
|
||||||
"Jean-françois SEMA",
|
"Jean-françois SEMA",
|
||||||
"3zS4QNQ4",
|
"3zS4QNQ4",
|
||||||
"Terminuz",
|
"Terminuz",
|
||||||
"Kurt",
|
"Kurt",
|
||||||
"ivistorm",
|
|
||||||
"Matt M.",
|
"Matt M.",
|
||||||
"Ivan Imes",
|
"Ivan Imes",
|
||||||
|
"J M",
|
||||||
|
"Bouya shaka",
|
||||||
"Faburizu",
|
"Faburizu",
|
||||||
"Jack Lawfield",
|
"Jack Lawfield",
|
||||||
"jimyjomson",
|
"jimyjomson",
|
||||||
"Borte",
|
"Borte",
|
||||||
|
"JaeHyun Jang",
|
||||||
"Chase Kwon",
|
"Chase Kwon",
|
||||||
"Ted Cart",
|
"yyuvuvu",
|
||||||
"Sage Himeros",
|
|
||||||
"Inyoshu",
|
"Inyoshu",
|
||||||
"Chad Barnes",
|
"Chad Barnes",
|
||||||
"Person Y",
|
"Person Y",
|
||||||
"David Spearing",
|
"Nomki",
|
||||||
"James Ming",
|
"James Ming",
|
||||||
"vanditking",
|
"vanditking",
|
||||||
"kripitonga",
|
"kripitonga",
|
||||||
@@ -787,5 +812,5 @@
|
|||||||
"Somebody",
|
"Somebody",
|
||||||
"CK"
|
"CK"
|
||||||
],
|
],
|
||||||
"totalCount": 784
|
"totalCount": 809
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,9 @@
|
|||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"menu": "Menü"
|
"menu": "Menü",
|
||||||
|
"remove": "Entfernen",
|
||||||
|
"change": "Ändern"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Wird geladen...",
|
"loading": "Wird geladen...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "Vorschau ersetzen",
|
"replacePreview": "Vorschau ersetzen",
|
||||||
"copyCheckpointName": "Checkpoint-Name kopieren",
|
"copyCheckpointName": "Checkpoint-Name kopieren",
|
||||||
"copyEmbeddingName": "Embedding-Name kopieren",
|
"copyEmbeddingName": "Embedding-Name kopieren",
|
||||||
|
"embeddingNameCopied": "Embedding-Syntax kopiert",
|
||||||
"sendCheckpointToWorkflow": "An ComfyUI senden",
|
"sendCheckpointToWorkflow": "An ComfyUI senden",
|
||||||
"sendEmbeddingToWorkflow": "An ComfyUI senden"
|
"sendEmbeddingToWorkflow": "An ComfyUI senden"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "Alle Syntax kopieren",
|
"copyAll": "Alle Syntax kopieren",
|
||||||
"refreshAll": "Alle Metadaten aktualisieren",
|
"refreshAll": "Alle Metadaten aktualisieren",
|
||||||
"repairMetadata": "Metadaten der Auswahl reparieren",
|
"repairMetadata": "Metadaten der Auswahl reparieren",
|
||||||
|
"reimportMetadata": "Aus Quelle neu importieren",
|
||||||
"checkUpdates": "Auswahl auf Updates prüfen",
|
"checkUpdates": "Auswahl auf Updates prüfen",
|
||||||
"moveAll": "Alle in Ordner verschieben",
|
"moveAll": "Alle in Ordner verschieben",
|
||||||
"autoOrganize": "Automatisch organisieren",
|
"autoOrganize": "Automatisch organisieren",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "Inhaltsbewertung festlegen",
|
"setContentRating": "Inhaltsbewertung festlegen",
|
||||||
"moveToFolder": "In Ordner verschieben",
|
"moveToFolder": "In Ordner verschieben",
|
||||||
"repairMetadata": "Metadaten reparieren",
|
"repairMetadata": "Metadaten reparieren",
|
||||||
|
"reimportMetadata": "Aus Quelle neu importieren",
|
||||||
"excludeModel": "Modell ausschließen",
|
"excludeModel": "Modell ausschließen",
|
||||||
"restoreModel": "Modell wiederherstellen",
|
"restoreModel": "Modell wiederherstellen",
|
||||||
"deleteModel": "Modell löschen",
|
"deleteModel": "Modell löschen",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "Rezept bereits in der neuesten Version, keine Reparatur erforderlich",
|
"skipped": "Rezept bereits in der neuesten Version, keine Reparatur erforderlich",
|
||||||
"failed": "Rezept-Reparatur fehlgeschlagen: {message}",
|
"failed": "Rezept-Reparatur fehlgeschlagen: {message}",
|
||||||
"missingId": "Rezept kann nicht repariert werden: Fehlende Rezept-ID"
|
"missingId": "Rezept kann nicht repariert werden: Fehlende Rezept-ID"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "Rezept wird aus Quelle neu importiert...",
|
||||||
|
"success": "Rezept erfolgreich neu importiert",
|
||||||
|
"noSourceUrl": "Rezept hat keine Quell-URL, Neuimport nicht möglich",
|
||||||
|
"failed": "Neuimport des Rezepts fehlgeschlagen: {message}",
|
||||||
|
"missingId": "Neuimport nicht möglich: Rezept-ID fehlt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Stammverzeichnis",
|
"modelRoot": "Stammverzeichnis",
|
||||||
|
"moreOptions": "Weitere Optionen",
|
||||||
"collapseAll": "Alle Ordner einklappen",
|
"collapseAll": "Alle Ordner einklappen",
|
||||||
"pinSidebar": "Sidebar anheften",
|
"pinSidebar": "Sidebar anheften",
|
||||||
"unpinSidebar": "Sidebar lösen",
|
"unpinSidebar": "Sidebar lösen",
|
||||||
|
"hideOnThisPage": "Seitenleiste auf dieser Seite ausblenden",
|
||||||
|
"showSidebar": "Seitenleiste anzeigen",
|
||||||
|
"sidebarHiddenNotification": "Seitenleiste auf der Seite {page} ausgeblendet",
|
||||||
"switchToListView": "Zur Listenansicht wechseln",
|
"switchToListView": "Zur Listenansicht wechseln",
|
||||||
"switchToTreeView": "Zur Baumansicht wechseln",
|
"switchToTreeView": "Zur Baumansicht wechseln",
|
||||||
"recursiveOn": "Unterordner einbeziehen",
|
"recursiveOn": "Unterordner einbeziehen",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "Modell von URL herunterladen",
|
"title": "Modell von URL herunterladen",
|
||||||
"titleWithType": "{type} von URL herunterladen",
|
"titleWithType": "{type} von URL herunterladen",
|
||||||
"url": "Civitai URL",
|
|
||||||
"civitaiUrl": "Civitai URL:",
|
"civitaiUrl": "Civitai URL:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "Geben Sie eine CivitAI- oder CivArchive-URL pro Zeile ein. Unterstützt mehrere URLs für den Batch-Download.",
|
||||||
"locationPreview": "Download-Speicherort Vorschau",
|
"locationPreview": "Download-Speicherort Vorschau",
|
||||||
"useDefaultPath": "Standardpfad verwenden",
|
"useDefaultPath": "Standardpfad verwenden",
|
||||||
"useDefaultPathTooltip": "Wenn aktiviert, werden Dateien automatisch mit konfigurierten Pfadvorlagen organisiert",
|
"useDefaultPathTooltip": "Wenn aktiviert, werden Dateien automatisch mit konfigurierten Pfadvorlagen organisiert",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Notizen erfolgreich gespeichert",
|
"saved": "Notizen erfolgreich gespeichert",
|
||||||
"saveFailed": "Fehler beim Speichern der Notizen"
|
"saveFailed": "Fehler beim Speichern der Notizen",
|
||||||
|
"showMore": "Mehr anzeigen",
|
||||||
|
"showLess": "Weniger anzeigen"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Voreingestellten Parameter hinzufügen...",
|
"addPresetParameter": "Voreingestellten Parameter hinzufügen...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "Keine kompatiblen Knoten im aktuellen Workflow verfügbar",
|
"noMatchingNodes": "Keine kompatiblen Knoten im aktuellen Workflow verfügbar",
|
||||||
"noTargetNodeSelected": "Kein Zielknoten ausgewählt",
|
"noTargetNodeSelected": "Kein Zielknoten ausgewählt",
|
||||||
"modelUpdated": "Modell im Workflow aktualisiert",
|
"modelUpdated": "Modell im Workflow aktualisiert",
|
||||||
"modelFailed": "Fehler beim Aktualisieren des Modellknotens"
|
"modelFailed": "Fehler beim Aktualisieren des Modellknotens",
|
||||||
|
"embeddingAdded": "Embedding zum Workflow hinzugefügt",
|
||||||
|
"embeddingFailed": "Fehler beim Hinzufügen des Embeddings"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "Rezept",
|
"recipe": "Rezept",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "Ersetzen",
|
"replace": "Ersetzen",
|
||||||
"append": "Anhängen",
|
"append": "Anhängen",
|
||||||
"selectTargetNode": "Zielknoten auswählen",
|
"selectTargetNode": "Zielknoten auswählen",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "Reparatur abgeschlossen: {repaired} repariert, {skipped} übersprungen (von {total})",
|
"repairBulkComplete": "Reparatur abgeschlossen: {repaired} repariert, {skipped} übersprungen (von {total})",
|
||||||
"repairBulkSkipped": "Keine Reparatur für die {total} ausgewählten Rezepte erforderlich",
|
"repairBulkSkipped": "Keine Reparatur für die {total} ausgewählten Rezepte erforderlich",
|
||||||
"repairBulkFailed": "Reparatur der ausgewählten Rezepte fehlgeschlagen: {message}",
|
"repairBulkFailed": "Reparatur der ausgewählten Rezepte fehlgeschlagen: {message}",
|
||||||
|
"reimporting": "Rezept wird aus Quelle neu importiert...",
|
||||||
|
"reimportSuccess": "Rezept erfolgreich neu importiert",
|
||||||
|
"reimportBulkComplete": "Neuimport abgeschlossen: {completed} importiert, {failed} fehlgeschlagen (von {total})",
|
||||||
|
"reimportBulkFailed": "Neuimport einiger Rezepte fehlgeschlagen",
|
||||||
"noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden",
|
"noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden",
|
||||||
"noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest."
|
"noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "Help",
|
"help": "Help",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"menu": "Menu"
|
"menu": "Menu",
|
||||||
|
"remove": "Remove",
|
||||||
|
"change": "Change"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "Replace Preview",
|
"replacePreview": "Replace Preview",
|
||||||
"copyCheckpointName": "Copy checkpoint name",
|
"copyCheckpointName": "Copy checkpoint name",
|
||||||
"copyEmbeddingName": "Copy embedding name",
|
"copyEmbeddingName": "Copy embedding name",
|
||||||
|
"embeddingNameCopied": "Embedding syntax copied",
|
||||||
"sendCheckpointToWorkflow": "Send to ComfyUI",
|
"sendCheckpointToWorkflow": "Send to ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Send to ComfyUI"
|
"sendEmbeddingToWorkflow": "Send to ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "Copy Selected Syntax",
|
"copyAll": "Copy Selected Syntax",
|
||||||
"refreshAll": "Refresh Selected Metadata",
|
"refreshAll": "Refresh Selected Metadata",
|
||||||
"repairMetadata": "Repair Metadata for Selected",
|
"repairMetadata": "Repair Metadata for Selected",
|
||||||
|
"reimportMetadata": "Re-import from Source",
|
||||||
"checkUpdates": "Check Updates for Selected",
|
"checkUpdates": "Check Updates for Selected",
|
||||||
"moveAll": "Move Selected to Folder",
|
"moveAll": "Move Selected to Folder",
|
||||||
"autoOrganize": "Auto-Organize Selected",
|
"autoOrganize": "Auto-Organize Selected",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "Set Content Rating",
|
"setContentRating": "Set Content Rating",
|
||||||
"moveToFolder": "Move to Folder",
|
"moveToFolder": "Move to Folder",
|
||||||
"repairMetadata": "Repair metadata",
|
"repairMetadata": "Repair metadata",
|
||||||
|
"reimportMetadata": "Re-import from Source",
|
||||||
"excludeModel": "Exclude Model",
|
"excludeModel": "Exclude Model",
|
||||||
"restoreModel": "Restore Model",
|
"restoreModel": "Restore Model",
|
||||||
"deleteModel": "Delete Model",
|
"deleteModel": "Delete Model",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "Recipe already at latest version, no repair needed",
|
"skipped": "Recipe already at latest version, no repair needed",
|
||||||
"failed": "Failed to repair recipe: {message}",
|
"failed": "Failed to repair recipe: {message}",
|
||||||
"missingId": "Cannot repair recipe: Missing recipe ID"
|
"missingId": "Cannot repair recipe: Missing recipe ID"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "Re-importing recipe from source...",
|
||||||
|
"success": "Recipe re-imported successfully",
|
||||||
|
"noSourceUrl": "Recipe has no source URL, cannot re-import",
|
||||||
|
"failed": "Failed to re-import recipe: {message}",
|
||||||
|
"missingId": "Cannot re-import recipe: Missing recipe ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Root",
|
"modelRoot": "Root",
|
||||||
|
"moreOptions": "More options",
|
||||||
"collapseAll": "Collapse All Folders",
|
"collapseAll": "Collapse All Folders",
|
||||||
"pinSidebar": "Pin Sidebar",
|
"pinSidebar": "Pin Sidebar",
|
||||||
"unpinSidebar": "Unpin Sidebar",
|
"unpinSidebar": "Unpin Sidebar",
|
||||||
|
"hideOnThisPage": "Hide sidebar on this page",
|
||||||
|
"showSidebar": "Show sidebar",
|
||||||
|
"sidebarHiddenNotification": "Folder sidebar hidden on {page} page",
|
||||||
"switchToListView": "Switch to List View",
|
"switchToListView": "Switch to List View",
|
||||||
"switchToTreeView": "Switch to Tree View",
|
"switchToTreeView": "Switch to Tree View",
|
||||||
"recursiveOn": "Include subfolders",
|
"recursiveOn": "Include subfolders",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "Download Model from URL",
|
"title": "Download Model from URL",
|
||||||
"titleWithType": "Download {type} from URL",
|
"titleWithType": "Download {type} from URL",
|
||||||
"url": "Civitai URL",
|
"civitaiUrl": "Civitai URL(s):",
|
||||||
"civitaiUrl": "Civitai URL:",
|
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "Enter one CivitAI or CivArchive URL per line. Supports multiple URLs for batch download.",
|
||||||
"locationPreview": "Download Location Preview",
|
"locationPreview": "Download Location Preview",
|
||||||
"useDefaultPath": "Use Default Path",
|
"useDefaultPath": "Use Default Path",
|
||||||
"useDefaultPathTooltip": "When enabled, files are automatically organized using configured path templates",
|
"useDefaultPathTooltip": "When enabled, files are automatically organized using configured path templates",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Notes saved successfully",
|
"saved": "Notes saved successfully",
|
||||||
"saveFailed": "Failed to save notes"
|
"saveFailed": "Failed to save notes",
|
||||||
|
"showMore": "Show more",
|
||||||
|
"showLess": "Show less"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Add preset parameter...",
|
"addPresetParameter": "Add preset parameter...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "No compatible nodes available in the current workflow",
|
"noMatchingNodes": "No compatible nodes available in the current workflow",
|
||||||
"noTargetNodeSelected": "No target node selected",
|
"noTargetNodeSelected": "No target node selected",
|
||||||
"modelUpdated": "Model updated in workflow",
|
"modelUpdated": "Model updated in workflow",
|
||||||
"modelFailed": "Failed to update model node"
|
"modelFailed": "Failed to update model node",
|
||||||
|
"embeddingAdded": "Embedding added to workflow",
|
||||||
|
"embeddingFailed": "Failed to add embedding"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "Recipe",
|
"recipe": "Recipe",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "Replace",
|
"replace": "Replace",
|
||||||
"append": "Append",
|
"append": "Append",
|
||||||
"selectTargetNode": "Select target node",
|
"selectTargetNode": "Select target node",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "Repair complete: {repaired} repaired, {skipped} skipped (of {total})",
|
"repairBulkComplete": "Repair complete: {repaired} repaired, {skipped} skipped (of {total})",
|
||||||
"repairBulkSkipped": "No repair needed for any of the {total} selected recipes",
|
"repairBulkSkipped": "No repair needed for any of the {total} selected recipes",
|
||||||
"repairBulkFailed": "Failed to repair selected recipes: {message}",
|
"repairBulkFailed": "Failed to repair selected recipes: {message}",
|
||||||
|
"reimporting": "Re-importing recipe from source...",
|
||||||
|
"reimportSuccess": "Recipe re-imported successfully",
|
||||||
|
"reimportBulkComplete": "Re-import complete: {completed} re-imported, {failed} failed (of {total})",
|
||||||
|
"reimportBulkFailed": "Failed to re-import some recipes",
|
||||||
"noMissingLorasInSelection": "No missing LoRAs found in selected recipes",
|
"noMissingLorasInSelection": "No missing LoRAs found in selected recipes",
|
||||||
"noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings."
|
"noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "Ayuda",
|
"help": "Ayuda",
|
||||||
"add": "Añadir",
|
"add": "Añadir",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"menu": "Menú"
|
"menu": "Menú",
|
||||||
|
"remove": "Eliminar",
|
||||||
|
"change": "Cambiar"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "Reemplazar vista previa",
|
"replacePreview": "Reemplazar vista previa",
|
||||||
"copyCheckpointName": "Copiar nombre del checkpoint",
|
"copyCheckpointName": "Copiar nombre del checkpoint",
|
||||||
"copyEmbeddingName": "Copiar nombre del embedding",
|
"copyEmbeddingName": "Copiar nombre del embedding",
|
||||||
|
"embeddingNameCopied": "Sintaxis de embedding copiada",
|
||||||
"sendCheckpointToWorkflow": "Enviar a ComfyUI",
|
"sendCheckpointToWorkflow": "Enviar a ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Enviar a ComfyUI"
|
"sendEmbeddingToWorkflow": "Enviar a ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "Copiar toda la sintaxis",
|
"copyAll": "Copiar toda la sintaxis",
|
||||||
"refreshAll": "Actualizar todos los metadatos",
|
"refreshAll": "Actualizar todos los metadatos",
|
||||||
"repairMetadata": "Reparar metadatos de la selección",
|
"repairMetadata": "Reparar metadatos de la selección",
|
||||||
|
"reimportMetadata": "Reimportar desde origen",
|
||||||
"checkUpdates": "Comprobar actualizaciones para la selección",
|
"checkUpdates": "Comprobar actualizaciones para la selección",
|
||||||
"moveAll": "Mover todos a carpeta",
|
"moveAll": "Mover todos a carpeta",
|
||||||
"autoOrganize": "Auto-organizar seleccionados",
|
"autoOrganize": "Auto-organizar seleccionados",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "Establecer clasificación de contenido",
|
"setContentRating": "Establecer clasificación de contenido",
|
||||||
"moveToFolder": "Mover a carpeta",
|
"moveToFolder": "Mover a carpeta",
|
||||||
"repairMetadata": "Reparar metadatos",
|
"repairMetadata": "Reparar metadatos",
|
||||||
|
"reimportMetadata": "Reimportar desde origen",
|
||||||
"excludeModel": "Excluir modelo",
|
"excludeModel": "Excluir modelo",
|
||||||
"restoreModel": "Restaurar modelo",
|
"restoreModel": "Restaurar modelo",
|
||||||
"deleteModel": "Eliminar modelo",
|
"deleteModel": "Eliminar modelo",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "La receta ya está en la última versión, no se necesita reparación",
|
"skipped": "La receta ya está en la última versión, no se necesita reparación",
|
||||||
"failed": "Error al reparar la receta: {message}",
|
"failed": "Error al reparar la receta: {message}",
|
||||||
"missingId": "No se puede reparar la receta: falta el ID de la receta"
|
"missingId": "No se puede reparar la receta: falta el ID de la receta"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "Reimportando receta desde origen...",
|
||||||
|
"success": "Receta reimportada exitosamente",
|
||||||
|
"noSourceUrl": "La receta no tiene URL de origen, no se puede reimportar",
|
||||||
|
"failed": "Error al reimportar la receta: {message}",
|
||||||
|
"missingId": "No se puede reimportar la receta: falta el ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Raíz",
|
"modelRoot": "Raíz",
|
||||||
|
"moreOptions": "Más opciones",
|
||||||
"collapseAll": "Colapsar todas las carpetas",
|
"collapseAll": "Colapsar todas las carpetas",
|
||||||
"pinSidebar": "Fijar barra lateral",
|
"pinSidebar": "Fijar barra lateral",
|
||||||
"unpinSidebar": "Desfijar barra lateral",
|
"unpinSidebar": "Desfijar barra lateral",
|
||||||
|
"hideOnThisPage": "Ocultar barra lateral en esta página",
|
||||||
|
"showSidebar": "Mostrar barra lateral",
|
||||||
|
"sidebarHiddenNotification": "Barra lateral oculta en la página {page}",
|
||||||
"switchToListView": "Cambiar a vista de lista",
|
"switchToListView": "Cambiar a vista de lista",
|
||||||
"switchToTreeView": "Cambiar a vista de árbol",
|
"switchToTreeView": "Cambiar a vista de árbol",
|
||||||
"recursiveOn": "Incluir subcarpetas",
|
"recursiveOn": "Incluir subcarpetas",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "Descargar modelo desde URL",
|
"title": "Descargar modelo desde URL",
|
||||||
"titleWithType": "Descargar {type} desde URL",
|
"titleWithType": "Descargar {type} desde URL",
|
||||||
"url": "URL de Civitai",
|
|
||||||
"civitaiUrl": "URL de Civitai:",
|
"civitaiUrl": "URL de Civitai:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "Ingrese una URL de CivitAI o CivArchive por línea. Admite múltiples URLs para descarga por lotes.",
|
||||||
"locationPreview": "Vista previa de ubicación de descarga",
|
"locationPreview": "Vista previa de ubicación de descarga",
|
||||||
"useDefaultPath": "Usar ruta predeterminada",
|
"useDefaultPath": "Usar ruta predeterminada",
|
||||||
"useDefaultPathTooltip": "Cuando está habilitado, los archivos se organizan automáticamente usando plantillas de rutas configuradas",
|
"useDefaultPathTooltip": "Cuando está habilitado, los archivos se organizan automáticamente usando plantillas de rutas configuradas",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Notas guardadas exitosamente",
|
"saved": "Notas guardadas exitosamente",
|
||||||
"saveFailed": "Error al guardar notas"
|
"saveFailed": "Error al guardar notas",
|
||||||
|
"showMore": "Mostrar más",
|
||||||
|
"showLess": "Mostrar menos"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Añadir parámetro preestablecido...",
|
"addPresetParameter": "Añadir parámetro preestablecido...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "No hay nodos compatibles disponibles en el flujo de trabajo actual",
|
"noMatchingNodes": "No hay nodos compatibles disponibles en el flujo de trabajo actual",
|
||||||
"noTargetNodeSelected": "No se ha seleccionado ningún nodo de destino",
|
"noTargetNodeSelected": "No se ha seleccionado ningún nodo de destino",
|
||||||
"modelUpdated": "Modelo actualizado en el flujo de trabajo",
|
"modelUpdated": "Modelo actualizado en el flujo de trabajo",
|
||||||
"modelFailed": "Error al actualizar nodo de modelo"
|
"modelFailed": "Error al actualizar nodo de modelo",
|
||||||
|
"embeddingAdded": "Embedding añadido al flujo de trabajo",
|
||||||
|
"embeddingFailed": "Error al añadir el embedding"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "Receta",
|
"recipe": "Receta",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "Reemplazar",
|
"replace": "Reemplazar",
|
||||||
"append": "Añadir",
|
"append": "Añadir",
|
||||||
"selectTargetNode": "Seleccionar nodo de destino",
|
"selectTargetNode": "Seleccionar nodo de destino",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "Reparación completa: {repaired} reparadas, {skipped} omitidas (de {total})",
|
"repairBulkComplete": "Reparación completa: {repaired} reparadas, {skipped} omitidas (de {total})",
|
||||||
"repairBulkSkipped": "No se necesita reparación para ninguna de las {total} recetas seleccionadas",
|
"repairBulkSkipped": "No se necesita reparación para ninguna de las {total} recetas seleccionadas",
|
||||||
"repairBulkFailed": "Error al reparar las recetas seleccionadas: {message}",
|
"repairBulkFailed": "Error al reparar las recetas seleccionadas: {message}",
|
||||||
|
"reimporting": "Reimportando receta desde origen...",
|
||||||
|
"reimportSuccess": "Receta reimportada exitosamente",
|
||||||
|
"reimportBulkComplete": "Reimportación completa: {completed} reimportadas, {failed} fallidas (de {total})",
|
||||||
|
"reimportBulkFailed": "Error al reimportar algunas recetas",
|
||||||
"noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas",
|
"noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas",
|
||||||
"noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración."
|
"noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"menu": "Menu"
|
"menu": "Menu",
|
||||||
|
"remove": "Supprimer",
|
||||||
|
"change": "Modifier"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "Remplacer l'aperçu",
|
"replacePreview": "Remplacer l'aperçu",
|
||||||
"copyCheckpointName": "Copier le nom du checkpoint",
|
"copyCheckpointName": "Copier le nom du checkpoint",
|
||||||
"copyEmbeddingName": "Copier le nom de l'embedding",
|
"copyEmbeddingName": "Copier le nom de l'embedding",
|
||||||
|
"embeddingNameCopied": "Syntaxe dembedding copiée",
|
||||||
"sendCheckpointToWorkflow": "Envoyer vers ComfyUI",
|
"sendCheckpointToWorkflow": "Envoyer vers ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Envoyer vers ComfyUI"
|
"sendEmbeddingToWorkflow": "Envoyer vers ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "Copier toute la syntaxe",
|
"copyAll": "Copier toute la syntaxe",
|
||||||
"refreshAll": "Actualiser toutes les métadonnées",
|
"refreshAll": "Actualiser toutes les métadonnées",
|
||||||
"repairMetadata": "Réparer les métadonnées de la sélection",
|
"repairMetadata": "Réparer les métadonnées de la sélection",
|
||||||
|
"reimportMetadata": "Ré-importer depuis la source",
|
||||||
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
"checkUpdates": "Vérifier les mises à jour pour la sélection",
|
||||||
"moveAll": "Déplacer tout vers un dossier",
|
"moveAll": "Déplacer tout vers un dossier",
|
||||||
"autoOrganize": "Auto-organiser la sélection",
|
"autoOrganize": "Auto-organiser la sélection",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "Définir la classification du contenu",
|
"setContentRating": "Définir la classification du contenu",
|
||||||
"moveToFolder": "Déplacer vers un dossier",
|
"moveToFolder": "Déplacer vers un dossier",
|
||||||
"repairMetadata": "Réparer les métadonnées",
|
"repairMetadata": "Réparer les métadonnées",
|
||||||
|
"reimportMetadata": "Ré-importer depuis la source",
|
||||||
"excludeModel": "Exclure le modèle",
|
"excludeModel": "Exclure le modèle",
|
||||||
"restoreModel": "Restaurer le modèle",
|
"restoreModel": "Restaurer le modèle",
|
||||||
"deleteModel": "Supprimer le modèle",
|
"deleteModel": "Supprimer le modèle",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "Recette déjà à la version la plus récente, aucune réparation nécessaire",
|
"skipped": "Recette déjà à la version la plus récente, aucune réparation nécessaire",
|
||||||
"failed": "Échec de la réparation de la recette : {message}",
|
"failed": "Échec de la réparation de la recette : {message}",
|
||||||
"missingId": "Impossible de réparer la recette : ID de recette manquant"
|
"missingId": "Impossible de réparer la recette : ID de recette manquant"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "Ré-import de la recette depuis la source...",
|
||||||
|
"success": "Recette ré-importée avec succès",
|
||||||
|
"noSourceUrl": "La recette n'a pas d'URL source, ré-import impossible",
|
||||||
|
"failed": "Échec du ré-import de la recette : {message}",
|
||||||
|
"missingId": "Impossible de ré-importer la recette : ID de recette manquant"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Racine",
|
"modelRoot": "Racine",
|
||||||
|
"moreOptions": "Plus d'options",
|
||||||
"collapseAll": "Réduire tous les dossiers",
|
"collapseAll": "Réduire tous les dossiers",
|
||||||
"pinSidebar": "Épingler la barre latérale",
|
"pinSidebar": "Épingler la barre latérale",
|
||||||
"unpinSidebar": "Désépingler la barre latérale",
|
"unpinSidebar": "Désépingler la barre latérale",
|
||||||
|
"hideOnThisPage": "Masquer la barre latérale sur cette page",
|
||||||
|
"showSidebar": "Afficher la barre latérale",
|
||||||
|
"sidebarHiddenNotification": "Barre latérale masquée sur la page {page}",
|
||||||
"switchToListView": "Passer en vue liste",
|
"switchToListView": "Passer en vue liste",
|
||||||
"switchToTreeView": "Passer en vue arborescence",
|
"switchToTreeView": "Passer en vue arborescence",
|
||||||
"recursiveOn": "Inclure les sous-dossiers",
|
"recursiveOn": "Inclure les sous-dossiers",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "Télécharger un modèle depuis une URL",
|
"title": "Télécharger un modèle depuis une URL",
|
||||||
"titleWithType": "Télécharger {type} depuis une URL",
|
"titleWithType": "Télécharger {type} depuis une URL",
|
||||||
"url": "URL Civitai",
|
|
||||||
"civitaiUrl": "URL Civitai :",
|
"civitaiUrl": "URL Civitai :",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "Entrez une URL CivitAI ou CivArchive par ligne. Prend en charge plusieurs URLs pour le téléchargement par lot.",
|
||||||
"locationPreview": "Aperçu de l'emplacement de téléchargement",
|
"locationPreview": "Aperçu de l'emplacement de téléchargement",
|
||||||
"useDefaultPath": "Utiliser le chemin par défaut",
|
"useDefaultPath": "Utiliser le chemin par défaut",
|
||||||
"useDefaultPathTooltip": "Lorsque activé, les fichiers sont automatiquement organisés selon les modèles de chemin configurés",
|
"useDefaultPathTooltip": "Lorsque activé, les fichiers sont automatiquement organisés selon les modèles de chemin configurés",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Notes sauvegardées avec succès",
|
"saved": "Notes sauvegardées avec succès",
|
||||||
"saveFailed": "Échec de la sauvegarde des notes"
|
"saveFailed": "Échec de la sauvegarde des notes",
|
||||||
|
"showMore": "Afficher plus",
|
||||||
|
"showLess": "Afficher moins"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Ajouter un paramètre prédéfini...",
|
"addPresetParameter": "Ajouter un paramètre prédéfini...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "Aucun nœud compatible disponible dans le workflow actuel",
|
"noMatchingNodes": "Aucun nœud compatible disponible dans le workflow actuel",
|
||||||
"noTargetNodeSelected": "Aucun nœud cible sélectionné",
|
"noTargetNodeSelected": "Aucun nœud cible sélectionné",
|
||||||
"modelUpdated": "Modèle mis à jour dans le workflow",
|
"modelUpdated": "Modèle mis à jour dans le workflow",
|
||||||
"modelFailed": "Échec de la mise à jour du nœud modèle"
|
"modelFailed": "Échec de la mise à jour du nœud modèle",
|
||||||
|
"embeddingAdded": "Embedding ajouté au workflow",
|
||||||
|
"embeddingFailed": "Échec de l'ajout de l'embedding"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "Recipe",
|
"recipe": "Recipe",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "Remplacer",
|
"replace": "Remplacer",
|
||||||
"append": "Ajouter",
|
"append": "Ajouter",
|
||||||
"selectTargetNode": "Sélectionner le nœud cible",
|
"selectTargetNode": "Sélectionner le nœud cible",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "Réparation terminée : {repaired} réparée(s), {skipped} ignorée(s) (sur {total})",
|
"repairBulkComplete": "Réparation terminée : {repaired} réparée(s), {skipped} ignorée(s) (sur {total})",
|
||||||
"repairBulkSkipped": "Aucune réparation nécessaire parmi les {total} recettes sélectionnées",
|
"repairBulkSkipped": "Aucune réparation nécessaire parmi les {total} recettes sélectionnées",
|
||||||
"repairBulkFailed": "Échec de la réparation des recettes sélectionnées : {message}",
|
"repairBulkFailed": "Échec de la réparation des recettes sélectionnées : {message}",
|
||||||
|
"reimporting": "Ré-import de la recette depuis la source...",
|
||||||
|
"reimportSuccess": "Recette ré-importée avec succès",
|
||||||
|
"reimportBulkComplete": "Ré-import terminé : {completed} ré-importé(s), {failed} échec(s) (sur {total})",
|
||||||
|
"reimportBulkFailed": "Échec du ré-import de certaines recettes",
|
||||||
"noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées",
|
"noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées",
|
||||||
"noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres."
|
"noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "עזרה",
|
"help": "עזרה",
|
||||||
"add": "הוספה",
|
"add": "הוספה",
|
||||||
"close": "סגור",
|
"close": "סגור",
|
||||||
"menu": "תפריט"
|
"menu": "תפריט",
|
||||||
|
"remove": "הסר",
|
||||||
|
"change": "שנה"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "טוען...",
|
"loading": "טוען...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "החלף תצוגה מקדימה",
|
"replacePreview": "החלף תצוגה מקדימה",
|
||||||
"copyCheckpointName": "העתק שם Checkpoint",
|
"copyCheckpointName": "העתק שם Checkpoint",
|
||||||
"copyEmbeddingName": "העתק שם Embedding",
|
"copyEmbeddingName": "העתק שם Embedding",
|
||||||
|
"embeddingNameCopied": "תחביר Embedding הועתק",
|
||||||
"sendCheckpointToWorkflow": "שלח ל-ComfyUI",
|
"sendCheckpointToWorkflow": "שלח ל-ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "שלח ל-ComfyUI"
|
"sendEmbeddingToWorkflow": "שלח ל-ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "העתק את כל התחבירים",
|
"copyAll": "העתק את כל התחבירים",
|
||||||
"refreshAll": "רענן את כל המטא-דאטה",
|
"refreshAll": "רענן את כל המטא-דאטה",
|
||||||
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
|
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
|
||||||
|
"reimportMetadata": "ייבא מחדש ממקור",
|
||||||
"checkUpdates": "בדוק עדכונים לבחירה",
|
"checkUpdates": "בדוק עדכונים לבחירה",
|
||||||
"moveAll": "העבר הכל לתיקייה",
|
"moveAll": "העבר הכל לתיקייה",
|
||||||
"autoOrganize": "ארגן אוטומטית נבחרים",
|
"autoOrganize": "ארגן אוטומטית נבחרים",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "הגדר דירוג תוכן",
|
"setContentRating": "הגדר דירוג תוכן",
|
||||||
"moveToFolder": "העבר לתיקייה",
|
"moveToFolder": "העבר לתיקייה",
|
||||||
"repairMetadata": "תיקון מטא-דאטה",
|
"repairMetadata": "תיקון מטא-דאטה",
|
||||||
|
"reimportMetadata": "ייבא מחדש ממקור",
|
||||||
"excludeModel": "החרג מודל",
|
"excludeModel": "החרג מודל",
|
||||||
"restoreModel": "שחזור מודל",
|
"restoreModel": "שחזור מודל",
|
||||||
"deleteModel": "מחק מודל",
|
"deleteModel": "מחק מודל",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "המתכון כבר בגרסה העדכנית ביותר, אין צורך בתיקון",
|
"skipped": "המתכון כבר בגרסה העדכנית ביותר, אין צורך בתיקון",
|
||||||
"failed": "תיקון המתכון נכשל: {message}",
|
"failed": "תיקון המתכון נכשל: {message}",
|
||||||
"missingId": "לא ניתן לתקן את המתכון: חסר מזהה מתכון"
|
"missingId": "לא ניתן לתקן את המתכון: חסר מזהה מתכון"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "מייבא מתכון מחדש מהמקור...",
|
||||||
|
"success": "המתכון יובא מחדש בהצלחה",
|
||||||
|
"noSourceUrl": "למתכון אין כתובת מקור, לא ניתן לייבא מחדש",
|
||||||
|
"failed": "ייבוא המתכון מחדש נכשל: {message}",
|
||||||
|
"missingId": "לא ניתן לייבא מחדש: חסר מזהה מתכון"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "שורש",
|
"modelRoot": "שורש",
|
||||||
|
"moreOptions": "אפשרויות נוספות",
|
||||||
"collapseAll": "כווץ את כל התיקיות",
|
"collapseAll": "כווץ את כל התיקיות",
|
||||||
"pinSidebar": "נעל סרגל צד",
|
"pinSidebar": "נעל סרגל צד",
|
||||||
"unpinSidebar": "שחרר סרגל צד",
|
"unpinSidebar": "שחרר סרגל צד",
|
||||||
|
"hideOnThisPage": "הסתר סרגל צד בדף זה",
|
||||||
|
"showSidebar": "הצג סרגל צד",
|
||||||
|
"sidebarHiddenNotification": "סרגל הצד מוסתר בדף {page}",
|
||||||
"switchToListView": "עבור לתצוגת רשימה",
|
"switchToListView": "עבור לתצוגת רשימה",
|
||||||
"switchToTreeView": "תצוגת עץ",
|
"switchToTreeView": "תצוגת עץ",
|
||||||
"recursiveOn": "כלול תיקיות משנה",
|
"recursiveOn": "כלול תיקיות משנה",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "הורד מודל מכתובת URL",
|
"title": "הורד מודל מכתובת URL",
|
||||||
"titleWithType": "הורד {type} מכתובת URL",
|
"titleWithType": "הורד {type} מכתובת URL",
|
||||||
"url": "כתובת URL של Civitai",
|
|
||||||
"civitaiUrl": "כתובת URL של Civitai:",
|
"civitaiUrl": "כתובת URL של Civitai:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "יש להזין כתובת URL אחת של CivitAI או CivArchive בכל שורה. תומך במספר כתובות URL להורדה בבת אחת.",
|
||||||
"locationPreview": "תצוגה מקדימה של מיקום ההורדה",
|
"locationPreview": "תצוגה מקדימה של מיקום ההורדה",
|
||||||
"useDefaultPath": "השתמש בנתיב ברירת מחדל",
|
"useDefaultPath": "השתמש בנתיב ברירת מחדל",
|
||||||
"useDefaultPathTooltip": "כאשר מופעל, קבצים מאורגנים אוטומטית באמצעות תבניות נתיב מוגדרות",
|
"useDefaultPathTooltip": "כאשר מופעל, קבצים מאורגנים אוטומטית באמצעות תבניות נתיב מוגדרות",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "הערות נשמרו בהצלחה",
|
"saved": "הערות נשמרו בהצלחה",
|
||||||
"saveFailed": "שמירת ההערות נכשלה"
|
"saveFailed": "שמירת ההערות נכשלה",
|
||||||
|
"showMore": "הצג עוד",
|
||||||
|
"showLess": "הצג פחות"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "הוסף פרמטר קבוע מראש...",
|
"addPresetParameter": "הוסף פרמטר קבוע מראש...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "אין צמתים תואמים זמינים ב-workflow הנוכחי",
|
"noMatchingNodes": "אין צמתים תואמים זמינים ב-workflow הנוכחי",
|
||||||
"noTargetNodeSelected": "לא נבחר צומת יעד",
|
"noTargetNodeSelected": "לא נבחר צומת יעד",
|
||||||
"modelUpdated": "מודל עודכן ב-workflow",
|
"modelUpdated": "מודל עודכן ב-workflow",
|
||||||
"modelFailed": "עדכון צומת המודל נכשל"
|
"modelFailed": "עדכון צומת המודל נכשל",
|
||||||
|
"embeddingAdded": "Embedding נוסף ל-workflow",
|
||||||
|
"embeddingFailed": "הוספת Embedding נכשלה"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "מתכון",
|
"recipe": "מתכון",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "החלף",
|
"replace": "החלף",
|
||||||
"append": "הוסף",
|
"append": "הוסף",
|
||||||
"selectTargetNode": "בחר צומת יעד",
|
"selectTargetNode": "בחר צומת יעד",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "התיקון הושלם: {repaired} תוקנו, {skipped} דולגו (מתוך {total})",
|
"repairBulkComplete": "התיקון הושלם: {repaired} תוקנו, {skipped} דולגו (מתוך {total})",
|
||||||
"repairBulkSkipped": "אין צורך בתיקון עבור {total} המתכונים הנבחרים",
|
"repairBulkSkipped": "אין צורך בתיקון עבור {total} המתכונים הנבחרים",
|
||||||
"repairBulkFailed": "תיקון המתכונים הנבחרים נכשל: {message}",
|
"repairBulkFailed": "תיקון המתכונים הנבחרים נכשל: {message}",
|
||||||
|
"reimporting": "מייבא מתכון מחדש מהמקור...",
|
||||||
|
"reimportSuccess": "המתכון יובא מחדש בהצלחה",
|
||||||
|
"reimportBulkComplete": "ייבוא מחדש הושלם: {completed} יובאו, {failed} נכשלו (מתוך {total})",
|
||||||
|
"reimportBulkFailed": "ייבוא מחדש של חלק מהמתכונים נכשל",
|
||||||
"noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו",
|
"noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו",
|
||||||
"noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות."
|
"noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "ヘルプ",
|
"help": "ヘルプ",
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"menu": "メニュー"
|
"menu": "メニュー",
|
||||||
|
"remove": "削除",
|
||||||
|
"change": "変更"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "プレビューを置換",
|
"replacePreview": "プレビューを置換",
|
||||||
"copyCheckpointName": "checkpoint名をコピー",
|
"copyCheckpointName": "checkpoint名をコピー",
|
||||||
"copyEmbeddingName": "embedding名をコピー",
|
"copyEmbeddingName": "embedding名をコピー",
|
||||||
|
"embeddingNameCopied": "Embedding構文をコピーしました",
|
||||||
"sendCheckpointToWorkflow": "ComfyUIに送信",
|
"sendCheckpointToWorkflow": "ComfyUIに送信",
|
||||||
"sendEmbeddingToWorkflow": "ComfyUIに送信"
|
"sendEmbeddingToWorkflow": "ComfyUIに送信"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "すべての構文をコピー",
|
"copyAll": "すべての構文をコピー",
|
||||||
"refreshAll": "すべてのメタデータを更新",
|
"refreshAll": "すべてのメタデータを更新",
|
||||||
"repairMetadata": "選択したレシピのメタデータを修復",
|
"repairMetadata": "選択したレシピのメタデータを修復",
|
||||||
|
"reimportMetadata": "ソースから再インポート",
|
||||||
"checkUpdates": "選択項目の更新を確認",
|
"checkUpdates": "選択項目の更新を確認",
|
||||||
"moveAll": "すべてをフォルダに移動",
|
"moveAll": "すべてをフォルダに移動",
|
||||||
"autoOrganize": "自動整理を実行",
|
"autoOrganize": "自動整理を実行",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "コンテンツレーティングを設定",
|
"setContentRating": "コンテンツレーティングを設定",
|
||||||
"moveToFolder": "フォルダに移動",
|
"moveToFolder": "フォルダに移動",
|
||||||
"repairMetadata": "メタデータを修復",
|
"repairMetadata": "メタデータを修復",
|
||||||
|
"reimportMetadata": "ソースから再インポート",
|
||||||
"excludeModel": "モデルを除外",
|
"excludeModel": "モデルを除外",
|
||||||
"restoreModel": "モデルを復元",
|
"restoreModel": "モデルを復元",
|
||||||
"deleteModel": "モデルを削除",
|
"deleteModel": "モデルを削除",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "レシピはすでに最新バージョンです。修復は不要です",
|
"skipped": "レシピはすでに最新バージョンです。修復は不要です",
|
||||||
"failed": "レシピの修復に失敗しました: {message}",
|
"failed": "レシピの修復に失敗しました: {message}",
|
||||||
"missingId": "レシピを修復できません: レシピIDがありません"
|
"missingId": "レシピを修復できません: レシピIDがありません"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "ソースからレシピを再インポート中...",
|
||||||
|
"success": "レシピの再インポートが完了しました",
|
||||||
|
"noSourceUrl": "レシピにソースURLがありません。再インポートできません",
|
||||||
|
"failed": "レシピの再インポートに失敗しました: {message}",
|
||||||
|
"missingId": "レシピを再インポートできません: レシピIDがありません"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "ルート",
|
"modelRoot": "ルート",
|
||||||
|
"moreOptions": "その他のオプション",
|
||||||
"collapseAll": "すべてのフォルダを折りたたむ",
|
"collapseAll": "すべてのフォルダを折りたたむ",
|
||||||
"pinSidebar": "サイドバーを固定",
|
"pinSidebar": "サイドバーを固定",
|
||||||
"unpinSidebar": "サイドバーの固定を解除",
|
"unpinSidebar": "サイドバーの固定を解除",
|
||||||
|
"hideOnThisPage": "このページでサイドバーを非表示",
|
||||||
|
"showSidebar": "サイドバーを表示",
|
||||||
|
"sidebarHiddenNotification": "{page}ページでサイドバーが非表示になっています",
|
||||||
"switchToListView": "リストビューに切り替え",
|
"switchToListView": "リストビューに切り替え",
|
||||||
"switchToTreeView": "ツリー表示に切り替え",
|
"switchToTreeView": "ツリー表示に切り替え",
|
||||||
"recursiveOn": "サブフォルダーを含める",
|
"recursiveOn": "サブフォルダーを含める",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "URLからモデルをダウンロード",
|
"title": "URLからモデルをダウンロード",
|
||||||
"titleWithType": "URLから{type}をダウンロード",
|
"titleWithType": "URLから{type}をダウンロード",
|
||||||
"url": "Civitai URL",
|
|
||||||
"civitaiUrl": "Civitai URL:",
|
"civitaiUrl": "Civitai URL:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "1行に1つのCivitAIまたはCivArchive URLを入力してください。複数のURLを一括ダウンロードできます。",
|
||||||
"locationPreview": "ダウンロード場所プレビュー",
|
"locationPreview": "ダウンロード場所プレビュー",
|
||||||
"useDefaultPath": "デフォルトパスを使用",
|
"useDefaultPath": "デフォルトパスを使用",
|
||||||
"useDefaultPathTooltip": "有効にすると、設定されたパステンプレートを使用してファイルが自動的に整理されます",
|
"useDefaultPathTooltip": "有効にすると、設定されたパステンプレートを使用してファイルが自動的に整理されます",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "メモが正常に保存されました",
|
"saved": "メモが正常に保存されました",
|
||||||
"saveFailed": "メモの保存に失敗しました"
|
"saveFailed": "メモの保存に失敗しました",
|
||||||
|
"showMore": "もっと見る",
|
||||||
|
"showLess": "折りたたむ"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "プリセットパラメータを追加...",
|
"addPresetParameter": "プリセットパラメータを追加...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "現在のワークフローには互換性のあるノードがありません",
|
"noMatchingNodes": "現在のワークフローには互換性のあるノードがありません",
|
||||||
"noTargetNodeSelected": "ターゲットノードが選択されていません",
|
"noTargetNodeSelected": "ターゲットノードが選択されていません",
|
||||||
"modelUpdated": "モデルがワークフローで更新されました",
|
"modelUpdated": "モデルがワークフローで更新されました",
|
||||||
"modelFailed": "モデルノードの更新に失敗しました"
|
"modelFailed": "モデルノードの更新に失敗しました",
|
||||||
|
"embeddingAdded": "Embeddingをワークフローに追加しました",
|
||||||
|
"embeddingFailed": "Embeddingの追加に失敗しました"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "レシピ",
|
"recipe": "レシピ",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "置換",
|
"replace": "置換",
|
||||||
"append": "追加",
|
"append": "追加",
|
||||||
"selectTargetNode": "ターゲットノードを選択",
|
"selectTargetNode": "ターゲットノードを選択",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "修復完了:{repaired} 件修復、{skipped} 件スキップ(合計 {total} 件)",
|
"repairBulkComplete": "修復完了:{repaired} 件修復、{skipped} 件スキップ(合計 {total} 件)",
|
||||||
"repairBulkSkipped": "選択した {total} 件のレシピは修復不要です",
|
"repairBulkSkipped": "選択した {total} 件のレシピは修復不要です",
|
||||||
"repairBulkFailed": "選択したレシピの修復に失敗しました:{message}",
|
"repairBulkFailed": "選択したレシピの修復に失敗しました:{message}",
|
||||||
|
"reimporting": "ソースからレシピを再インポート中...",
|
||||||
|
"reimportSuccess": "レシピの再インポートが完了しました",
|
||||||
|
"reimportBulkComplete": "再インポート完了:{completed} 件成功、{failed} 件失敗(合計 {total} 件)",
|
||||||
|
"reimportBulkFailed": "一部のレシピの再インポートに失敗しました",
|
||||||
"noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした",
|
"noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした",
|
||||||
"noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。"
|
"noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "도움말",
|
"help": "도움말",
|
||||||
"add": "추가",
|
"add": "추가",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"menu": "메뉴"
|
"menu": "메뉴",
|
||||||
|
"remove": "제거",
|
||||||
|
"change": "변경"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "미리보기 교체",
|
"replacePreview": "미리보기 교체",
|
||||||
"copyCheckpointName": "Checkpoint 이름 복사",
|
"copyCheckpointName": "Checkpoint 이름 복사",
|
||||||
"copyEmbeddingName": "Embedding 이름 복사",
|
"copyEmbeddingName": "Embedding 이름 복사",
|
||||||
|
"embeddingNameCopied": "Embedding 구문 복사됨",
|
||||||
"sendCheckpointToWorkflow": "ComfyUI로 전송",
|
"sendCheckpointToWorkflow": "ComfyUI로 전송",
|
||||||
"sendEmbeddingToWorkflow": "ComfyUI로 전송"
|
"sendEmbeddingToWorkflow": "ComfyUI로 전송"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "모든 문법 복사",
|
"copyAll": "모든 문법 복사",
|
||||||
"refreshAll": "모든 메타데이터 새로고침",
|
"refreshAll": "모든 메타데이터 새로고침",
|
||||||
"repairMetadata": "선택한 레시피 메타데이터 복구",
|
"repairMetadata": "선택한 레시피 메타데이터 복구",
|
||||||
|
"reimportMetadata": "소스에서 다시 가져오기",
|
||||||
"checkUpdates": "선택 항목 업데이트 확인",
|
"checkUpdates": "선택 항목 업데이트 확인",
|
||||||
"moveAll": "모두 폴더로 이동",
|
"moveAll": "모두 폴더로 이동",
|
||||||
"autoOrganize": "자동 정리 선택",
|
"autoOrganize": "자동 정리 선택",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "콘텐츠 등급 설정",
|
"setContentRating": "콘텐츠 등급 설정",
|
||||||
"moveToFolder": "폴더로 이동",
|
"moveToFolder": "폴더로 이동",
|
||||||
"repairMetadata": "메타데이터 복구",
|
"repairMetadata": "메타데이터 복구",
|
||||||
|
"reimportMetadata": "소스에서 다시 가져오기",
|
||||||
"excludeModel": "모델 제외",
|
"excludeModel": "모델 제외",
|
||||||
"restoreModel": "모델 복원",
|
"restoreModel": "모델 복원",
|
||||||
"deleteModel": "모델 삭제",
|
"deleteModel": "모델 삭제",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "레시피가 이미 최신 버전입니다. 복구가 필요하지 않습니다",
|
"skipped": "레시피가 이미 최신 버전입니다. 복구가 필요하지 않습니다",
|
||||||
"failed": "레시피 복구 실패: {message}",
|
"failed": "레시피 복구 실패: {message}",
|
||||||
"missingId": "레시피를 복구할 수 없음: 레시피 ID 누락"
|
"missingId": "레시피를 복구할 수 없음: 레시피 ID 누락"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "소스에서 레시피를 다시 가져오는 중...",
|
||||||
|
"success": "레시피를 다시 가져왔습니다",
|
||||||
|
"noSourceUrl": "레시피에 소스 URL이 없어 다시 가져올 수 없습니다",
|
||||||
|
"failed": "레시피 다시 가져오기 실패: {message}",
|
||||||
|
"missingId": "레시피를 다시 가져올 수 없음: 레시피 ID 누락"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "루트",
|
"modelRoot": "루트",
|
||||||
|
"moreOptions": "더 많은 옵션",
|
||||||
"collapseAll": "모든 폴더 접기",
|
"collapseAll": "모든 폴더 접기",
|
||||||
"pinSidebar": "사이드바 고정",
|
"pinSidebar": "사이드바 고정",
|
||||||
"unpinSidebar": "사이드바 고정 해제",
|
"unpinSidebar": "사이드바 고정 해제",
|
||||||
|
"hideOnThisPage": "이 페이지에서 사이드바 숨기기",
|
||||||
|
"showSidebar": "사이드바 표시",
|
||||||
|
"sidebarHiddenNotification": "{page} 페이지에서 사이드바가 숨겨져 있습니다",
|
||||||
"switchToListView": "목록 보기로 전환",
|
"switchToListView": "목록 보기로 전환",
|
||||||
"switchToTreeView": "트리 보기로 전환",
|
"switchToTreeView": "트리 보기로 전환",
|
||||||
"recursiveOn": "하위 폴더 포함",
|
"recursiveOn": "하위 폴더 포함",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "URL에서 모델 다운로드",
|
"title": "URL에서 모델 다운로드",
|
||||||
"titleWithType": "URL에서 {type} 다운로드",
|
"titleWithType": "URL에서 {type} 다운로드",
|
||||||
"url": "Civitai URL",
|
|
||||||
"civitaiUrl": "Civitai URL:",
|
"civitaiUrl": "Civitai URL:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "한 줄에 하나의 CivitAI 또는 CivArchive URL을 입력하세요. 여러 URL을 일괄 다운로드할 수 있습니다.",
|
||||||
"locationPreview": "다운로드 위치 미리보기",
|
"locationPreview": "다운로드 위치 미리보기",
|
||||||
"useDefaultPath": "기본 경로 사용",
|
"useDefaultPath": "기본 경로 사용",
|
||||||
"useDefaultPathTooltip": "활성화하면 구성된 경로 템플릿을 사용하여 파일이 자동으로 정리됩니다",
|
"useDefaultPathTooltip": "활성화하면 구성된 경로 템플릿을 사용하여 파일이 자동으로 정리됩니다",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "메모가 성공적으로 저장됨",
|
"saved": "메모가 성공적으로 저장됨",
|
||||||
"saveFailed": "메모 저장 실패"
|
"saveFailed": "메모 저장 실패",
|
||||||
|
"showMore": "더 보기",
|
||||||
|
"showLess": "접기"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "프리셋 매개변수 추가...",
|
"addPresetParameter": "프리셋 매개변수 추가...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "현재 워크플로에서 호환되는 노드가 없습니다",
|
"noMatchingNodes": "현재 워크플로에서 호환되는 노드가 없습니다",
|
||||||
"noTargetNodeSelected": "대상 노드가 선택되지 않았습니다",
|
"noTargetNodeSelected": "대상 노드가 선택되지 않았습니다",
|
||||||
"modelUpdated": "모델이 워크플로에서 업데이트되었습니다",
|
"modelUpdated": "모델이 워크플로에서 업데이트되었습니다",
|
||||||
"modelFailed": "모델 노드 업데이트 실패"
|
"modelFailed": "모델 노드 업데이트 실패",
|
||||||
|
"embeddingAdded": "Embedding을 워크플로에 추가했습니다",
|
||||||
|
"embeddingFailed": "Embedding 추가 실패"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "레시피",
|
"recipe": "레시피",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "임베딩",
|
||||||
"replace": "교체",
|
"replace": "교체",
|
||||||
"append": "추가",
|
"append": "추가",
|
||||||
"selectTargetNode": "대상 노드 선택",
|
"selectTargetNode": "대상 노드 선택",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "복구 완료: {repaired}개 복구, {skipped}개 건너뜀 (총 {total}개)",
|
"repairBulkComplete": "복구 완료: {repaired}개 복구, {skipped}개 건너뜀 (총 {total}개)",
|
||||||
"repairBulkSkipped": "선택한 {total}개 레시피는 복구가 필요하지 않습니다",
|
"repairBulkSkipped": "선택한 {total}개 레시피는 복구가 필요하지 않습니다",
|
||||||
"repairBulkFailed": "선택한 레시피 복구 실패: {message}",
|
"repairBulkFailed": "선택한 레시피 복구 실패: {message}",
|
||||||
|
"reimporting": "소스에서 레시피를 다시 가져오는 중...",
|
||||||
|
"reimportSuccess": "레시피를 다시 가져왔습니다",
|
||||||
|
"reimportBulkComplete": "다시 가져오기 완료: {completed}개 성공, {failed}개 실패 (총 {total}개)",
|
||||||
|
"reimportBulkFailed": "일부 레시피를 다시 가져오지 못했습니다",
|
||||||
"noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다",
|
"noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다",
|
||||||
"noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요."
|
"noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "Справка",
|
"help": "Справка",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"menu": "Меню"
|
"menu": "Меню",
|
||||||
|
"remove": "Удалить",
|
||||||
|
"change": "Изменить"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "Заменить превью",
|
"replacePreview": "Заменить превью",
|
||||||
"copyCheckpointName": "Копировать имя checkpoint",
|
"copyCheckpointName": "Копировать имя checkpoint",
|
||||||
"copyEmbeddingName": "Копировать имя embedding",
|
"copyEmbeddingName": "Копировать имя embedding",
|
||||||
|
"embeddingNameCopied": "Синтаксис embedding скопирован",
|
||||||
"sendCheckpointToWorkflow": "Отправить в ComfyUI",
|
"sendCheckpointToWorkflow": "Отправить в ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Отправить в ComfyUI"
|
"sendEmbeddingToWorkflow": "Отправить в ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "Копировать весь синтаксис",
|
"copyAll": "Копировать весь синтаксис",
|
||||||
"refreshAll": "Обновить все метаданные",
|
"refreshAll": "Обновить все метаданные",
|
||||||
"repairMetadata": "Восстановить метаданные для выбранных",
|
"repairMetadata": "Восстановить метаданные для выбранных",
|
||||||
|
"reimportMetadata": "Переимпортировать из источника",
|
||||||
"checkUpdates": "Проверить обновления для выбранных",
|
"checkUpdates": "Проверить обновления для выбранных",
|
||||||
"moveAll": "Переместить все в папку",
|
"moveAll": "Переместить все в папку",
|
||||||
"autoOrganize": "Автоматически организовать выбранные",
|
"autoOrganize": "Автоматически организовать выбранные",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "Установить рейтинг контента",
|
"setContentRating": "Установить рейтинг контента",
|
||||||
"moveToFolder": "Переместить в папку",
|
"moveToFolder": "Переместить в папку",
|
||||||
"repairMetadata": "Восстановить метаданные",
|
"repairMetadata": "Восстановить метаданные",
|
||||||
|
"reimportMetadata": "Переимпортировать из источника",
|
||||||
"excludeModel": "Исключить модель",
|
"excludeModel": "Исключить модель",
|
||||||
"restoreModel": "Восстановить модель",
|
"restoreModel": "Восстановить модель",
|
||||||
"deleteModel": "Удалить модель",
|
"deleteModel": "Удалить модель",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "Рецепт уже последней версии, восстановление не требуется",
|
"skipped": "Рецепт уже последней версии, восстановление не требуется",
|
||||||
"failed": "Не удалось восстановить рецепт: {message}",
|
"failed": "Не удалось восстановить рецепт: {message}",
|
||||||
"missingId": "Не удалось восстановить рецепт: отсутствует ID рецепта"
|
"missingId": "Не удалось восстановить рецепт: отсутствует ID рецепта"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "Переимпорт рецепта из источника...",
|
||||||
|
"success": "Рецепт успешно переимпортирован",
|
||||||
|
"noSourceUrl": "У рецепта нет URL источника, переимпорт невозможен",
|
||||||
|
"failed": "Не удалось переимпортировать рецепт: {message}",
|
||||||
|
"missingId": "Невозможно переимпортировать рецепт: отсутствует ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Корень",
|
"modelRoot": "Корень",
|
||||||
|
"moreOptions": "Дополнительные параметры",
|
||||||
"collapseAll": "Свернуть все папки",
|
"collapseAll": "Свернуть все папки",
|
||||||
"pinSidebar": "Закрепить боковую панель",
|
"pinSidebar": "Закрепить боковую панель",
|
||||||
"unpinSidebar": "Открепить боковую панель",
|
"unpinSidebar": "Открепить боковую панель",
|
||||||
|
"hideOnThisPage": "Скрыть боковую панель на этой странице",
|
||||||
|
"showSidebar": "Показать боковую панель",
|
||||||
|
"sidebarHiddenNotification": "Боковая панель скрыта на странице {page}",
|
||||||
"switchToListView": "Переключить на вид списка",
|
"switchToListView": "Переключить на вид списка",
|
||||||
"switchToTreeView": "Переключить на древовидный вид",
|
"switchToTreeView": "Переключить на древовидный вид",
|
||||||
"recursiveOn": "Включать вложенные папки",
|
"recursiveOn": "Включать вложенные папки",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "Скачать модель по URL",
|
"title": "Скачать модель по URL",
|
||||||
"titleWithType": "Скачать {type} по URL",
|
"titleWithType": "Скачать {type} по URL",
|
||||||
"url": "Civitai URL",
|
|
||||||
"civitaiUrl": "Civitai URL:",
|
"civitaiUrl": "Civitai URL:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "Введите один URL CivitAI или CivArchive в каждой строке. Поддерживается пакетная загрузка нескольких URL.",
|
||||||
"locationPreview": "Предпросмотр места загрузки",
|
"locationPreview": "Предпросмотр места загрузки",
|
||||||
"useDefaultPath": "Использовать путь по умолчанию",
|
"useDefaultPath": "Использовать путь по умолчанию",
|
||||||
"useDefaultPathTooltip": "При включении файлы автоматически организуются с использованием настроенных шаблонов путей",
|
"useDefaultPathTooltip": "При включении файлы автоматически организуются с использованием настроенных шаблонов путей",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Заметки успешно сохранены",
|
"saved": "Заметки успешно сохранены",
|
||||||
"saveFailed": "Не удалось сохранить заметки"
|
"saveFailed": "Не удалось сохранить заметки",
|
||||||
|
"showMore": "Показать больше",
|
||||||
|
"showLess": "Свернуть"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Добавить предустановленный параметр...",
|
"addPresetParameter": "Добавить предустановленный параметр...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "В текущем workflow нет совместимых узлов",
|
"noMatchingNodes": "В текущем workflow нет совместимых узлов",
|
||||||
"noTargetNodeSelected": "Целевой узел не выбран",
|
"noTargetNodeSelected": "Целевой узел не выбран",
|
||||||
"modelUpdated": "Модель обновлена в workflow",
|
"modelUpdated": "Модель обновлена в workflow",
|
||||||
"modelFailed": "Не удалось обновить узел модели"
|
"modelFailed": "Не удалось обновить узел модели",
|
||||||
|
"embeddingAdded": "Embedding добавлен в workflow",
|
||||||
|
"embeddingFailed": "Не удалось добавить embedding"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "Рецепт",
|
"recipe": "Рецепт",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Эмбеддинг",
|
||||||
"replace": "Заменить",
|
"replace": "Заменить",
|
||||||
"append": "Добавить",
|
"append": "Добавить",
|
||||||
"selectTargetNode": "Выберите целевой узел",
|
"selectTargetNode": "Выберите целевой узел",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "Восстановление завершено: {repaired} восстановлено, {skipped} пропущено (из {total})",
|
"repairBulkComplete": "Восстановление завершено: {repaired} восстановлено, {skipped} пропущено (из {total})",
|
||||||
"repairBulkSkipped": "Ни один из {total} выбранных рецептов не требует восстановления",
|
"repairBulkSkipped": "Ни один из {total} выбранных рецептов не требует восстановления",
|
||||||
"repairBulkFailed": "Не удалось восстановить выбранные рецепты: {message}",
|
"repairBulkFailed": "Не удалось восстановить выбранные рецепты: {message}",
|
||||||
|
"reimporting": "Переимпорт рецепта из источника...",
|
||||||
|
"reimportSuccess": "Рецепт успешно переимпортирован",
|
||||||
|
"reimportBulkComplete": "Переимпорт завершён: {completed} переимпортировано, {failed} ошибок (из {total})",
|
||||||
|
"reimportBulkFailed": "Не удалось переимпортировать некоторые рецепты",
|
||||||
"noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs",
|
"noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs",
|
||||||
"noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках."
|
"noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"menu": "菜单"
|
"menu": "菜单",
|
||||||
|
"remove": "移除",
|
||||||
|
"change": "更换"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "替换预览",
|
"replacePreview": "替换预览",
|
||||||
"copyCheckpointName": "复制 Checkpoint 名称",
|
"copyCheckpointName": "复制 Checkpoint 名称",
|
||||||
"copyEmbeddingName": "复制 Embedding 名称",
|
"copyEmbeddingName": "复制 Embedding 名称",
|
||||||
|
"embeddingNameCopied": "已复制 Embedding 语法",
|
||||||
"sendCheckpointToWorkflow": "发送到 ComfyUI",
|
"sendCheckpointToWorkflow": "发送到 ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "发送到 ComfyUI"
|
"sendEmbeddingToWorkflow": "发送到 ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "复制所选中语法",
|
"copyAll": "复制所选中语法",
|
||||||
"refreshAll": "刷新所选中元数据",
|
"refreshAll": "刷新所选中元数据",
|
||||||
"repairMetadata": "修复所选中元数据",
|
"repairMetadata": "修复所选中元数据",
|
||||||
|
"reimportMetadata": "从源重新导入",
|
||||||
"checkUpdates": "检查所选更新",
|
"checkUpdates": "检查所选更新",
|
||||||
"moveAll": "移动所选中到文件夹",
|
"moveAll": "移动所选中到文件夹",
|
||||||
"autoOrganize": "自动整理所选模型",
|
"autoOrganize": "自动整理所选模型",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "设置内容评级",
|
"setContentRating": "设置内容评级",
|
||||||
"moveToFolder": "移动到文件夹",
|
"moveToFolder": "移动到文件夹",
|
||||||
"repairMetadata": "修复元数据",
|
"repairMetadata": "修复元数据",
|
||||||
|
"reimportMetadata": "从源重新导入",
|
||||||
"excludeModel": "排除模型",
|
"excludeModel": "排除模型",
|
||||||
"restoreModel": "恢复模型",
|
"restoreModel": "恢复模型",
|
||||||
"deleteModel": "删除模型",
|
"deleteModel": "删除模型",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "配方已是最新版本,无需修复",
|
"skipped": "配方已是最新版本,无需修复",
|
||||||
"failed": "修复配方失败:{message}",
|
"failed": "修复配方失败:{message}",
|
||||||
"missingId": "无法修复配方:缺少配方 ID"
|
"missingId": "无法修复配方:缺少配方 ID"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "正在从源重新导入配方...",
|
||||||
|
"success": "配方已从源重新导入成功",
|
||||||
|
"noSourceUrl": "配方没有源URL,无法重新导入",
|
||||||
|
"failed": "重新导入配方失败:{message}",
|
||||||
|
"missingId": "无法重新导入配方:缺少配方ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目录",
|
"modelRoot": "根目录",
|
||||||
|
"moreOptions": "更多选项",
|
||||||
"collapseAll": "折叠所有文件夹",
|
"collapseAll": "折叠所有文件夹",
|
||||||
"pinSidebar": "固定侧边栏",
|
"pinSidebar": "固定侧边栏",
|
||||||
"unpinSidebar": "取消固定侧边栏",
|
"unpinSidebar": "取消固定侧边栏",
|
||||||
|
"hideOnThisPage": "隐藏此页面侧边栏",
|
||||||
|
"showSidebar": "显示侧边栏",
|
||||||
|
"sidebarHiddenNotification": "{page}页面的文件夹侧边栏已隐藏",
|
||||||
"switchToListView": "切换到列表视图",
|
"switchToListView": "切换到列表视图",
|
||||||
"switchToTreeView": "切换到树状视图",
|
"switchToTreeView": "切换到树状视图",
|
||||||
"recursiveOn": "包含子文件夹",
|
"recursiveOn": "包含子文件夹",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "从 URL 下载模型",
|
"title": "从 URL 下载模型",
|
||||||
"titleWithType": "从 URL 下载 {type}",
|
"titleWithType": "从 URL 下载 {type}",
|
||||||
"url": "Civitai URL",
|
|
||||||
"civitaiUrl": "Civitai URL:",
|
"civitaiUrl": "Civitai URL:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "每行输入一个 CivitAI 或 CivArchive URL。支持批量下载多个 URL。",
|
||||||
"locationPreview": "下载位置预览",
|
"locationPreview": "下载位置预览",
|
||||||
"useDefaultPath": "使用默认路径",
|
"useDefaultPath": "使用默认路径",
|
||||||
"useDefaultPathTooltip": "启用后,文件将自动按配置的路径模板进行整理",
|
"useDefaultPathTooltip": "启用后,文件将自动按配置的路径模板进行整理",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "备注保存成功",
|
"saved": "备注保存成功",
|
||||||
"saveFailed": "备注保存失败"
|
"saveFailed": "备注保存失败",
|
||||||
|
"showMore": "展开",
|
||||||
|
"showLess": "收起"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "添加预设参数...",
|
"addPresetParameter": "添加预设参数...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "当前工作流中没有兼容的节点",
|
"noMatchingNodes": "当前工作流中没有兼容的节点",
|
||||||
"noTargetNodeSelected": "未选择目标节点",
|
"noTargetNodeSelected": "未选择目标节点",
|
||||||
"modelUpdated": "模型已更新到工作流",
|
"modelUpdated": "模型已更新到工作流",
|
||||||
"modelFailed": "更新模型节点失败"
|
"modelFailed": "更新模型节点失败",
|
||||||
|
"embeddingAdded": "Embedding 已追加到工作流",
|
||||||
|
"embeddingFailed": "添加 Embedding 失败"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "配方",
|
"recipe": "配方",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "替换",
|
"replace": "替换",
|
||||||
"append": "追加",
|
"append": "追加",
|
||||||
"selectTargetNode": "选择目标节点",
|
"selectTargetNode": "选择目标节点",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "修复完成:{repaired} 个已修复,{skipped} 个已跳过(共 {total} 个)",
|
"repairBulkComplete": "修复完成:{repaired} 个已修复,{skipped} 个已跳过(共 {total} 个)",
|
||||||
"repairBulkSkipped": "所选 {total} 个配方无需修复",
|
"repairBulkSkipped": "所选 {total} 个配方无需修复",
|
||||||
"repairBulkFailed": "修复所选配方失败:{message}",
|
"repairBulkFailed": "修复所选配方失败:{message}",
|
||||||
|
"reimporting": "正在从源重新导入配方...",
|
||||||
|
"reimportSuccess": "配方已从源重新导入成功",
|
||||||
|
"reimportBulkComplete": "重新导入完成:{completed} 个已导入,{failed} 个失败(共 {total} 个)",
|
||||||
|
"reimportBulkFailed": "重新导入某些配方失败",
|
||||||
"noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs",
|
"noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs",
|
||||||
"noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。"
|
"noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "說明",
|
"help": "說明",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"menu": "選單"
|
"menu": "選單",
|
||||||
|
"remove": "移除",
|
||||||
|
"change": "更換"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
@@ -111,6 +113,7 @@
|
|||||||
"replacePreview": "更換預覽圖",
|
"replacePreview": "更換預覽圖",
|
||||||
"copyCheckpointName": "複製檢查點名稱",
|
"copyCheckpointName": "複製檢查點名稱",
|
||||||
"copyEmbeddingName": "複製嵌入名稱",
|
"copyEmbeddingName": "複製嵌入名稱",
|
||||||
|
"embeddingNameCopied": "已複製 Embedding 語法",
|
||||||
"sendCheckpointToWorkflow": "傳送到 ComfyUI",
|
"sendCheckpointToWorkflow": "傳送到 ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "傳送到 ComfyUI"
|
"sendEmbeddingToWorkflow": "傳送到 ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -690,6 +693,7 @@
|
|||||||
"copyAll": "複製全部語法",
|
"copyAll": "複製全部語法",
|
||||||
"refreshAll": "刷新全部 metadata",
|
"refreshAll": "刷新全部 metadata",
|
||||||
"repairMetadata": "修復所選中元數據",
|
"repairMetadata": "修復所選中元數據",
|
||||||
|
"reimportMetadata": "從來源重新匯入",
|
||||||
"checkUpdates": "檢查所選更新",
|
"checkUpdates": "檢查所選更新",
|
||||||
"moveAll": "全部移動到資料夾",
|
"moveAll": "全部移動到資料夾",
|
||||||
"autoOrganize": "自動整理所選模型",
|
"autoOrganize": "自動整理所選模型",
|
||||||
@@ -737,6 +741,7 @@
|
|||||||
"setContentRating": "設定內容分級",
|
"setContentRating": "設定內容分級",
|
||||||
"moveToFolder": "移動到資料夾",
|
"moveToFolder": "移動到資料夾",
|
||||||
"repairMetadata": "修復元數據",
|
"repairMetadata": "修復元數據",
|
||||||
|
"reimportMetadata": "從來源重新匯入",
|
||||||
"excludeModel": "排除模型",
|
"excludeModel": "排除模型",
|
||||||
"restoreModel": "還原模型",
|
"restoreModel": "還原模型",
|
||||||
"deleteModel": "刪除模型",
|
"deleteModel": "刪除模型",
|
||||||
@@ -864,6 +869,13 @@
|
|||||||
"skipped": "配方已是最新版本,無需修復",
|
"skipped": "配方已是最新版本,無需修復",
|
||||||
"failed": "修復配方失敗:{message}",
|
"failed": "修復配方失敗:{message}",
|
||||||
"missingId": "無法修復配方:缺少配方 ID"
|
"missingId": "無法修復配方:缺少配方 ID"
|
||||||
|
},
|
||||||
|
"reimport": {
|
||||||
|
"starting": "正在從來源重新匯入配方...",
|
||||||
|
"success": "配方已從來源重新匯入成功",
|
||||||
|
"noSourceUrl": "配方沒有來源URL,無法重新匯入",
|
||||||
|
"failed": "重新匯入配方失敗:{message}",
|
||||||
|
"missingId": "無法重新匯入配方:缺少配方ID"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
@@ -941,9 +953,13 @@
|
|||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目錄",
|
"modelRoot": "根目錄",
|
||||||
|
"moreOptions": "更多選項",
|
||||||
"collapseAll": "全部摺疊資料夾",
|
"collapseAll": "全部摺疊資料夾",
|
||||||
"pinSidebar": "固定側邊欄",
|
"pinSidebar": "固定側邊欄",
|
||||||
"unpinSidebar": "取消固定側邊欄",
|
"unpinSidebar": "取消固定側邊欄",
|
||||||
|
"hideOnThisPage": "隱藏此頁面側邊欄",
|
||||||
|
"showSidebar": "顯示側邊欄",
|
||||||
|
"sidebarHiddenNotification": "{page}頁面的資料夾側邊欄已隱藏",
|
||||||
"switchToListView": "切換至列表檢視",
|
"switchToListView": "切換至列表檢視",
|
||||||
"switchToTreeView": "切換到樹狀檢視",
|
"switchToTreeView": "切換到樹狀檢視",
|
||||||
"recursiveOn": "包含子資料夾",
|
"recursiveOn": "包含子資料夾",
|
||||||
@@ -1014,9 +1030,9 @@
|
|||||||
"download": {
|
"download": {
|
||||||
"title": "從網址下載模型",
|
"title": "從網址下載模型",
|
||||||
"titleWithType": "從網址下載 {type}",
|
"titleWithType": "從網址下載 {type}",
|
||||||
"url": "Civitai 網址",
|
|
||||||
"civitaiUrl": "Civitai 網址:",
|
"civitaiUrl": "Civitai 網址:",
|
||||||
"placeholder": "https://civitai.com/models/...",
|
"placeholder": "https://civitai.com/models/...",
|
||||||
|
"urlHint": "每行輸入一個 CivitAI 或 CivArchive URL。支援批量下載多個 URL。",
|
||||||
"locationPreview": "下載位置預覽",
|
"locationPreview": "下載位置預覽",
|
||||||
"useDefaultPath": "使用預設路徑",
|
"useDefaultPath": "使用預設路徑",
|
||||||
"useDefaultPathTooltip": "啟用後,檔案將依照設定的路徑範本自動整理",
|
"useDefaultPathTooltip": "啟用後,檔案將依照設定的路徑範本自動整理",
|
||||||
@@ -1225,7 +1241,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "備註已儲存",
|
"saved": "備註已儲存",
|
||||||
"saveFailed": "儲存備註失敗"
|
"saveFailed": "儲存備註失敗",
|
||||||
|
"showMore": "展開",
|
||||||
|
"showLess": "收起"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "新增預設參數...",
|
"addPresetParameter": "新增預設參數...",
|
||||||
@@ -1487,11 +1505,14 @@
|
|||||||
"noMatchingNodes": "目前工作流程中沒有相容的節點",
|
"noMatchingNodes": "目前工作流程中沒有相容的節點",
|
||||||
"noTargetNodeSelected": "未選擇目標節點",
|
"noTargetNodeSelected": "未選擇目標節點",
|
||||||
"modelUpdated": "模型已更新到工作流",
|
"modelUpdated": "模型已更新到工作流",
|
||||||
"modelFailed": "更新模型節點失敗"
|
"modelFailed": "更新模型節點失敗",
|
||||||
|
"embeddingAdded": "Embedding 已附加到工作流",
|
||||||
|
"embeddingFailed": "傳送 Embedding 到工作流失敗"
|
||||||
},
|
},
|
||||||
"nodeSelector": {
|
"nodeSelector": {
|
||||||
"recipe": "配方",
|
"recipe": "配方",
|
||||||
"lora": "LoRA",
|
"lora": "LoRA",
|
||||||
|
"embedding": "Embedding",
|
||||||
"replace": "取代",
|
"replace": "取代",
|
||||||
"append": "附加",
|
"append": "附加",
|
||||||
"selectTargetNode": "選擇目標節點",
|
"selectTargetNode": "選擇目標節點",
|
||||||
@@ -1713,6 +1734,10 @@
|
|||||||
"repairBulkComplete": "修復完成:{repaired} 個已修復,{skipped} 個已跳過(共 {total} 個)",
|
"repairBulkComplete": "修復完成:{repaired} 個已修復,{skipped} 個已跳過(共 {total} 個)",
|
||||||
"repairBulkSkipped": "所選 {total} 個配方無需修復",
|
"repairBulkSkipped": "所選 {total} 個配方無需修復",
|
||||||
"repairBulkFailed": "修復所選配方失敗:{message}",
|
"repairBulkFailed": "修復所選配方失敗:{message}",
|
||||||
|
"reimporting": "正在從來源重新匯入配方...",
|
||||||
|
"reimportSuccess": "配方已從來源重新匯入成功",
|
||||||
|
"reimportBulkComplete": "重新匯入完成:{completed} 個已匯入,{failed} 個失敗(共 {total} 個)",
|
||||||
|
"reimportBulkFailed": "重新匯入某些配方失敗",
|
||||||
"noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs",
|
"noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs",
|
||||||
"noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。"
|
"noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -189,6 +189,10 @@ class LoraManager:
|
|||||||
|
|
||||||
# Register DownloadManager with ServiceRegistry
|
# Register DownloadManager with ServiceRegistry
|
||||||
await ServiceRegistry.get_download_manager()
|
await ServiceRegistry.get_download_manager()
|
||||||
|
|
||||||
|
# Initialize DownloadQueueService for persistent queue/history
|
||||||
|
await ServiceRegistry.get_download_queue_service()
|
||||||
|
|
||||||
await ServiceRegistry.get_backup_service()
|
await ServiceRegistry.get_backup_service()
|
||||||
|
|
||||||
from .services.metadata_service import initialize_metadata_providers
|
from .services.metadata_service import initialize_metadata_providers
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ MODELS = "models"
|
|||||||
PROMPTS = "prompts"
|
PROMPTS = "prompts"
|
||||||
SAMPLING = "sampling"
|
SAMPLING = "sampling"
|
||||||
LORAS = "loras"
|
LORAS = "loras"
|
||||||
|
EMBEDDINGS = "embeddings"
|
||||||
SIZE = "size"
|
SIZE = "size"
|
||||||
IMAGES = "images"
|
IMAGES = "images"
|
||||||
IS_SAMPLER = "is_sampler" # New constant to mark sampler nodes
|
IS_SAMPLER = "is_sampler" # New constant to mark sampler nodes
|
||||||
|
|
||||||
# Complete list of categories to track
|
# Complete list of categories to track
|
||||||
METADATA_CATEGORIES = [MODELS, PROMPTS, SAMPLING, LORAS, SIZE, IMAGES]
|
METADATA_CATEGORIES = [MODELS, PROMPTS, SAMPLING, LORAS, EMBEDDINGS, SIZE, IMAGES]
|
||||||
|
|||||||
@@ -3086,6 +3086,7 @@ class NodeRegistryHandler:
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
widget_name = data.get("widget_name")
|
widget_name = data.get("widget_name")
|
||||||
value = data.get("value")
|
value = data.get("value")
|
||||||
|
mode = data.get("mode", "replace")
|
||||||
node_ids = data.get("node_ids")
|
node_ids = data.get("node_ids")
|
||||||
|
|
||||||
if not isinstance(widget_name, str) or not widget_name:
|
if not isinstance(widget_name, str) or not widget_name:
|
||||||
@@ -3133,6 +3134,7 @@ class NodeRegistryHandler:
|
|||||||
"id": parsed_node_id,
|
"id": parsed_node_id,
|
||||||
"widget_name": widget_name,
|
"widget_name": widget_name,
|
||||||
"value": value,
|
"value": value,
|
||||||
|
"mode": mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph_identifier is not None:
|
if graph_identifier is not None:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from ...services.use_cases import (
|
|||||||
)
|
)
|
||||||
from ...services.websocket_manager import WebSocketManager
|
from ...services.websocket_manager import WebSocketManager
|
||||||
from ...services.websocket_progress_callback import WebSocketProgressCallback
|
from ...services.websocket_progress_callback import WebSocketProgressCallback
|
||||||
|
from ...services.download_queue_service import DownloadQueueService
|
||||||
from ...services.errors import RateLimitError, ResourceNotFoundError
|
from ...services.errors import RateLimitError, ResourceNotFoundError
|
||||||
from ...utils.civitai_utils import resolve_license_payload
|
from ...utils.civitai_utils import resolve_license_payload
|
||||||
from ...utils.file_utils import calculate_sha256
|
from ...utils.file_utils import calculate_sha256
|
||||||
@@ -1567,6 +1568,255 @@ class ModelDownloadHandler:
|
|||||||
)
|
)
|
||||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Download queue / history handlers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def get_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
queue = await service.get_queue()
|
||||||
|
stats = await service.get_stats()
|
||||||
|
return web.json_response({"success": True, "queue": queue, "stats": stats})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def add_to_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
download_id = request.query.get("download_id") or str(uuid.uuid4())
|
||||||
|
model_id_str = request.query.get("model_id")
|
||||||
|
model_version_id_str = request.query.get("model_version_id")
|
||||||
|
model_name = request.query.get("model_name", "")
|
||||||
|
version_name = request.query.get("version_name", "")
|
||||||
|
thumbnail_url = request.query.get("thumbnail_url", "")
|
||||||
|
source = request.query.get("source")
|
||||||
|
file_params_json = request.query.get("file_params")
|
||||||
|
|
||||||
|
model_id = int(model_id_str) if model_id_str else None
|
||||||
|
model_version_id = int(model_version_id_str) if model_version_id_str else None
|
||||||
|
file_params = json.loads(file_params_json) if file_params_json else None
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.add_to_queue(
|
||||||
|
download_id=download_id,
|
||||||
|
model_id=model_id,
|
||||||
|
model_version_id=model_version_id,
|
||||||
|
model_name=model_name,
|
||||||
|
version_name=version_name,
|
||||||
|
thumbnail_url=thumbnail_url,
|
||||||
|
source=source,
|
||||||
|
file_params=file_params,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error adding to download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def remove_from_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
removed = await service.remove_from_queue(download_id)
|
||||||
|
return web.json_response({"success": removed})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error removing from download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def move_queue_item_to_top(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
moved = await service.move_to_top(download_id)
|
||||||
|
return web.json_response({"success": moved})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error moving queue item to top: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def move_queue_item_to_end(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
moved = await service.move_to_end(download_id)
|
||||||
|
return web.json_response({"success": moved})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error moving queue item to end: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def clear_download_queue(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cleared = await service.clear_queue(status_filter=status_filter)
|
||||||
|
return web.json_response({"success": True, "cleared": cleared})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error clearing download queue: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def get_download_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
limit = min(int(request.query.get("limit", "50")), 500)
|
||||||
|
offset = int(request.query.get("offset", "0"))
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
result = await service.get_history(
|
||||||
|
limit=limit, offset=offset, status_filter=status_filter
|
||||||
|
)
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"items": result["items"],
|
||||||
|
"total": result["total"],
|
||||||
|
"limit": result["limit"],
|
||||||
|
"offset": result["offset"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def clear_download_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
status_filter = request.query.get("status") or None
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cleared = await service.clear_history(status_filter=status_filter)
|
||||||
|
return web.json_response({"success": True, "cleared": cleared})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error clearing download history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def delete_download_history_item(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
item_id = int(request.query.get("id", "0"))
|
||||||
|
if not item_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
deleted = await service.delete_history_item(item_id)
|
||||||
|
return web.json_response({"success": deleted})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error deleting download history item: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def retry_download_from_history(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
item_id = int(request.query.get("id", "0"))
|
||||||
|
if not item_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "id is required"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.retry_from_history(item_id)
|
||||||
|
if item is None:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "History item not found or not retryable"},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error retrying download from history: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def retry_all_failed_downloads(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
retry_count = await service.retry_all_failed()
|
||||||
|
return web.json_response({"success": True, "retry_count": retry_count})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error retrying all failed downloads: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def complete_download_in_queue(self, request: web.Request) -> web.Response:
|
||||||
|
"""Atomically move a download from queue to history with terminal status."""
|
||||||
|
try:
|
||||||
|
download_id = request.query.get("download_id")
|
||||||
|
if not download_id:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "download_id is required"}, status=400
|
||||||
|
)
|
||||||
|
status = request.query.get("status", "completed")
|
||||||
|
error = request.query.get("error")
|
||||||
|
file_path = request.query.get("file_path")
|
||||||
|
try:
|
||||||
|
bytes_downloaded = int(request.query.get("bytes_downloaded", "0"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
bytes_downloaded = 0
|
||||||
|
total_bytes_raw = request.query.get("total_bytes")
|
||||||
|
total_bytes = int(total_bytes_raw) if total_bytes_raw else None
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
item = await service.complete_download(
|
||||||
|
download_id=download_id,
|
||||||
|
status=status,
|
||||||
|
error=error,
|
||||||
|
file_path=file_path,
|
||||||
|
bytes_downloaded=bytes_downloaded,
|
||||||
|
total_bytes=total_bytes,
|
||||||
|
)
|
||||||
|
if item is None:
|
||||||
|
return web.json_response(
|
||||||
|
{"success": False, "error": "Download not found in queue"}, status=404
|
||||||
|
)
|
||||||
|
return web.json_response({"success": True, "item": item})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error completing download: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def get_download_stats(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
stats = await service.get_stats()
|
||||||
|
return web.json_response({"success": True, "stats": stats})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error getting download stats: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
class ModelCivitaiHandler:
|
class ModelCivitaiHandler:
|
||||||
"""CivitAI integration endpoints."""
|
"""CivitAI integration endpoints."""
|
||||||
@@ -2016,10 +2266,21 @@ class ModelUpdateHandler:
|
|||||||
self._logger.error("Failed to refresh model updates: %s", exc, exc_info=True)
|
self._logger.error("Failed to refresh model updates: %s", exc, exc_info=True)
|
||||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
hide_early_access = False
|
||||||
|
if self._settings is not None:
|
||||||
|
try:
|
||||||
|
hide_early_access = bool(
|
||||||
|
self._settings.get("hide_early_access_updates", False)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
serialized_records = []
|
serialized_records = []
|
||||||
for record in records.values():
|
for record in records.values():
|
||||||
has_update_fn = getattr(record, "has_update", None)
|
has_update_fn = getattr(record, "has_update", None)
|
||||||
if callable(has_update_fn) and has_update_fn():
|
if callable(has_update_fn) and has_update_fn(
|
||||||
|
hide_early_access=hide_early_access
|
||||||
|
):
|
||||||
serialized_records.append(self._serialize_record(record))
|
serialized_records.append(self._serialize_record(record))
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -2585,6 +2846,19 @@ class ModelHandlerSet:
|
|||||||
"pause_download_get": self.download.pause_download_get,
|
"pause_download_get": self.download.pause_download_get,
|
||||||
"resume_download_get": self.download.resume_download_get,
|
"resume_download_get": self.download.resume_download_get,
|
||||||
"get_download_progress": self.download.get_download_progress,
|
"get_download_progress": self.download.get_download_progress,
|
||||||
|
"get_download_queue": self.download.get_download_queue,
|
||||||
|
"add_to_download_queue": self.download.add_to_download_queue,
|
||||||
|
"remove_from_download_queue": self.download.remove_from_download_queue,
|
||||||
|
"move_queue_item_to_top": self.download.move_queue_item_to_top,
|
||||||
|
"move_queue_item_to_end": self.download.move_queue_item_to_end,
|
||||||
|
"clear_download_queue": self.download.clear_download_queue,
|
||||||
|
"get_download_history": self.download.get_download_history,
|
||||||
|
"clear_download_history": self.download.clear_download_history,
|
||||||
|
"delete_download_history_item": self.download.delete_download_history_item,
|
||||||
|
"retry_download_from_history": self.download.retry_download_from_history,
|
||||||
|
"retry_all_failed_downloads": self.download.retry_all_failed_downloads,
|
||||||
|
"complete_download_in_queue": self.download.complete_download_in_queue,
|
||||||
|
"get_download_stats": self.download.get_download_stats,
|
||||||
"get_civitai_versions": self.civitai.get_civitai_versions,
|
"get_civitai_versions": self.civitai.get_civitai_versions,
|
||||||
"get_civitai_model_by_version": self.civitai.get_civitai_model_by_version,
|
"get_civitai_model_by_version": self.civitai.get_civitai_model_by_version,
|
||||||
"get_civitai_model_by_hash": self.civitai.get_civitai_model_by_hash,
|
"get_civitai_model_by_hash": self.civitai.get_civitai_model_by_hash,
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class RecipeHandlerSet:
|
|||||||
"check_image_exists": self.management.check_image_exists,
|
"check_image_exists": self.management.check_image_exists,
|
||||||
"import_from_url": self.management.import_from_url,
|
"import_from_url": self.management.import_from_url,
|
||||||
"create_from_example": self.management.create_from_example,
|
"create_from_example": self.management.create_from_example,
|
||||||
|
"reimport_recipe": self.management.reimport_recipe,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -799,6 +800,126 @@ class RecipeManagementHandler:
|
|||||||
self._logger.error("Error repairing single recipe: %s", exc, exc_info=True)
|
self._logger.error("Error repairing single recipe: %s", exc, exc_info=True)
|
||||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
async def reimport_recipe(self, request: web.Request) -> web.Response:
|
||||||
|
"""Delete a recipe and re-import it from its source URL.
|
||||||
|
|
||||||
|
This gives the recipe a fresh start — re-downloads the image from
|
||||||
|
CivitAI, re-parses EXIF metadata with the current parser, and
|
||||||
|
re-resolves LoRAs / checkpoint. User edits (title, tags, favorite)
|
||||||
|
are carried over from the old recipe.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await self._ensure_dependencies_ready()
|
||||||
|
recipe_scanner = self._recipe_scanner_getter()
|
||||||
|
if recipe_scanner is None:
|
||||||
|
raise RuntimeError("Recipe scanner unavailable")
|
||||||
|
|
||||||
|
recipe_id = request.match_info["recipe_id"]
|
||||||
|
old_recipe = await recipe_scanner.get_recipe_by_id(recipe_id)
|
||||||
|
if not old_recipe:
|
||||||
|
raise RecipeNotFoundError(f"Recipe {recipe_id} not found")
|
||||||
|
|
||||||
|
source_path = old_recipe.get("source_path")
|
||||||
|
if not source_path:
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": (
|
||||||
|
"Recipe has no source URL — cannot re-import. "
|
||||||
|
"Use repair or manual import instead."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_edits: dict[str, Any] = {}
|
||||||
|
for key in ("title", "tags", "favorite", "preview_nsfw_level"):
|
||||||
|
if key in old_recipe and old_recipe[key] is not None:
|
||||||
|
user_edits[key] = old_recipe[key]
|
||||||
|
if "tags" in user_edits and not isinstance(user_edits["tags"], list):
|
||||||
|
del user_edits["tags"]
|
||||||
|
|
||||||
|
old_file_path = old_recipe.get("file_path", "")
|
||||||
|
old_folder = os.path.dirname(old_file_path) if old_file_path else None
|
||||||
|
|
||||||
|
image_id = extract_civitai_image_id(source_path)
|
||||||
|
is_local_file = not image_id and os.path.isfile(source_path)
|
||||||
|
|
||||||
|
if not image_id and not is_local_file:
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": (
|
||||||
|
"Recipe source is neither a valid CivitAI image URL "
|
||||||
|
"nor an accessible local file. "
|
||||||
|
"Use repair or manual import instead."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_local_file:
|
||||||
|
return await self._do_reimport_from_local(
|
||||||
|
source_path,
|
||||||
|
recipe_scanner,
|
||||||
|
recipe_id=recipe_id,
|
||||||
|
target_dir=old_folder,
|
||||||
|
user_edits=user_edits,
|
||||||
|
old_title=old_recipe.get("title", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
async with self._import_semaphore:
|
||||||
|
import_response = await self._do_import_from_url(
|
||||||
|
source_path,
|
||||||
|
recipe_scanner,
|
||||||
|
target_dir=old_folder,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._persistence_service.delete_recipe(
|
||||||
|
recipe_scanner=recipe_scanner, recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
body_bytes = import_response.body
|
||||||
|
if not body_bytes:
|
||||||
|
raise RuntimeError("Re-import returned an empty response")
|
||||||
|
import_body = json.loads(body_bytes.decode())
|
||||||
|
new_recipe_id = import_body.get("recipe_id")
|
||||||
|
|
||||||
|
if new_recipe_id and user_edits:
|
||||||
|
try:
|
||||||
|
await self._persistence_service.update_recipe(
|
||||||
|
recipe_scanner=recipe_scanner,
|
||||||
|
recipe_id=new_recipe_id,
|
||||||
|
updates=user_edits,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.warning(
|
||||||
|
"Re-import succeeded but failed to carry over "
|
||||||
|
"user edits for new recipe %s: %s",
|
||||||
|
new_recipe_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"old_recipe_id": recipe_id,
|
||||||
|
"recipe_id": new_recipe_id,
|
||||||
|
"source_path": source_path,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except RecipeNotFoundError as exc:
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=404)
|
||||||
|
except RecipeValidationError as exc:
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=400)
|
||||||
|
except RecipeDownloadError as exc:
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=400)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error(
|
||||||
|
"Error reimporting recipe: %s", exc, exc_info=True
|
||||||
|
)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
async def get_repair_progress(self, request: web.Request) -> web.Response:
|
async def get_repair_progress(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
progress = self._ws_manager.get_recipe_repair_progress()
|
progress = self._ws_manager.get_recipe_repair_progress()
|
||||||
@@ -907,6 +1028,7 @@ class RecipeManagementHandler:
|
|||||||
extension,
|
extension,
|
||||||
civitai_meta_raw,
|
civitai_meta_raw,
|
||||||
model_version_id,
|
model_version_id,
|
||||||
|
_original_image_url,
|
||||||
) = await self._download_remote_media(image_url)
|
) = await self._download_remote_media(image_url)
|
||||||
|
|
||||||
# Extract embedded EXIF metadata (offloaded to thread pool in this call)
|
# Extract embedded EXIF metadata (offloaded to thread pool in this call)
|
||||||
@@ -1319,7 +1441,9 @@ class RecipeManagementHandler:
|
|||||||
"exclude": False,
|
"exclude": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _download_remote_media(self, image_url: str) -> tuple[bytes, str, Any, Any]:
|
async def _download_remote_media(
|
||||||
|
self, image_url: str
|
||||||
|
) -> tuple[bytes, str, Any, Any, Optional[str]]:
|
||||||
civitai_client = self._civitai_client_getter()
|
civitai_client = self._civitai_client_getter()
|
||||||
downloader = await self._downloader_factory()
|
downloader = await self._downloader_factory()
|
||||||
temp_path = None
|
temp_path = None
|
||||||
@@ -1394,11 +1518,16 @@ class RecipeManagementHandler:
|
|||||||
if mvids and isinstance(civitai_meta_raw, dict):
|
if mvids and isinstance(civitai_meta_raw, dict):
|
||||||
civitai_meta_raw["modelVersionIds"] = mvids
|
civitai_meta_raw["modelVersionIds"] = mvids
|
||||||
|
|
||||||
|
original_url = (
|
||||||
|
image_info.get("url") if civitai_image_id and image_info else None
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
file_obj.read(),
|
file_obj.read(),
|
||||||
extension,
|
extension,
|
||||||
civitai_meta_raw,
|
civitai_meta_raw,
|
||||||
model_ver_id,
|
model_ver_id,
|
||||||
|
original_url,
|
||||||
)
|
)
|
||||||
except RecipeDownloadError:
|
except RecipeDownloadError:
|
||||||
raise
|
raise
|
||||||
@@ -1543,6 +1672,9 @@ class RecipeManagementHandler:
|
|||||||
self,
|
self,
|
||||||
image_url: str,
|
image_url: str,
|
||||||
recipe_scanner: Any,
|
recipe_scanner: Any,
|
||||||
|
*,
|
||||||
|
recipe_id: str | None = None,
|
||||||
|
target_dir: str | None = None,
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
image_id = extract_civitai_image_id(image_url)
|
image_id = extract_civitai_image_id(image_url)
|
||||||
if not image_id:
|
if not image_id:
|
||||||
@@ -1550,7 +1682,7 @@ class RecipeManagementHandler:
|
|||||||
"Could not extract Civitai image ID from URL"
|
"Could not extract Civitai image ID from URL"
|
||||||
)
|
)
|
||||||
|
|
||||||
image_bytes, extension, civitai_meta_raw, model_version_id = (
|
image_bytes, extension, civitai_meta_raw, model_version_id, original_image_url = (
|
||||||
await self._download_remote_media(image_url)
|
await self._download_remote_media(image_url)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1588,6 +1720,51 @@ class RecipeManagementHandler:
|
|||||||
"Failed to extract embedded metadata: %s", exc
|
"Failed to extract embedded metadata: %s", exc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not parsed_embedded and original_image_url:
|
||||||
|
self._logger.debug(
|
||||||
|
"Optimized image has no embedded metadata, "
|
||||||
|
"falling back to original: %s",
|
||||||
|
original_image_url,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
downloader = await self._downloader_factory()
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
suffix=".png", delete=False
|
||||||
|
) as tmp:
|
||||||
|
orig_tmp_path = tmp.name
|
||||||
|
try:
|
||||||
|
success, _ = await downloader.download_file(
|
||||||
|
original_image_url, orig_tmp_path, use_auth=False
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
raw_orig = await asyncio.to_thread(
|
||||||
|
ExifUtils.extract_image_metadata, orig_tmp_path
|
||||||
|
)
|
||||||
|
if raw_orig:
|
||||||
|
parser = (
|
||||||
|
self._analysis_service._recipe_parser_factory.create_parser(
|
||||||
|
raw_orig
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if parser:
|
||||||
|
parsed_embedded = await parser.parse_metadata(
|
||||||
|
raw_orig, recipe_scanner=recipe_scanner
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
parsed_embedded
|
||||||
|
and "gen_params" in parsed_embedded
|
||||||
|
):
|
||||||
|
embedded_gen_params = parsed_embedded[
|
||||||
|
"gen_params"
|
||||||
|
]
|
||||||
|
finally:
|
||||||
|
if os.path.exists(orig_tmp_path):
|
||||||
|
os.unlink(orig_tmp_path)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.warning(
|
||||||
|
"Failed to extract metadata from original image: %s", exc
|
||||||
|
)
|
||||||
|
|
||||||
# Parse CivitAI API meta to discover all resources from modelVersionIds.
|
# Parse CivitAI API meta to discover all resources from modelVersionIds.
|
||||||
# Run unconditionally — EXIF parsing succeeds for gen_params but misses
|
# Run unconditionally — EXIF parsing succeeds for gen_params but misses
|
||||||
# LoRAs (modelVersionIds is NOT in the image EXIF).
|
# LoRAs (modelVersionIds is NOT in the image EXIF).
|
||||||
@@ -1671,9 +1848,104 @@ class RecipeManagementHandler:
|
|||||||
tags=[],
|
tags=[],
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
extension=extension,
|
extension=extension,
|
||||||
|
recipe_id=recipe_id,
|
||||||
|
target_dir=target_dir,
|
||||||
)
|
)
|
||||||
return web.json_response(result.payload, status=result.status)
|
return web.json_response(result.payload, status=result.status)
|
||||||
|
|
||||||
|
async def _do_reimport_from_local(
|
||||||
|
self,
|
||||||
|
file_path: str,
|
||||||
|
recipe_scanner: Any,
|
||||||
|
*,
|
||||||
|
recipe_id: str,
|
||||||
|
target_dir: str | None,
|
||||||
|
user_edits: dict[str, Any],
|
||||||
|
old_title: str,
|
||||||
|
) -> web.Response:
|
||||||
|
"""Re-import a recipe from a local image file.
|
||||||
|
|
||||||
|
Reads the original source file, re-parses its EXIF metadata, saves a
|
||||||
|
fresh recipe, then deletes the old one.
|
||||||
|
"""
|
||||||
|
normalized = os.path.normpath(file_path)
|
||||||
|
if not os.path.isfile(normalized):
|
||||||
|
raise RecipeNotFoundError(
|
||||||
|
f"Source file no longer accessible: {normalized}"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(normalized, "rb") as fh:
|
||||||
|
image_bytes = fh.read()
|
||||||
|
|
||||||
|
extension = os.path.splitext(normalized)[1].lower() or ".png"
|
||||||
|
|
||||||
|
analysis_result = await self._analysis_service.analyze_local_image(
|
||||||
|
file_path=normalized,
|
||||||
|
recipe_scanner=recipe_scanner,
|
||||||
|
)
|
||||||
|
analysis_payload: dict[str, Any] = analysis_result.payload
|
||||||
|
|
||||||
|
gen_params = analysis_payload.get("gen_params") or {}
|
||||||
|
loras = analysis_payload.get("loras") or []
|
||||||
|
checkpoint = analysis_payload.get("checkpoint")
|
||||||
|
base_model = analysis_payload.get("base_model", "")
|
||||||
|
|
||||||
|
metadata: dict[str, Any] = {
|
||||||
|
"base_model": base_model,
|
||||||
|
"loras": loras,
|
||||||
|
"gen_params": gen_params,
|
||||||
|
"source_path": normalized,
|
||||||
|
}
|
||||||
|
if checkpoint:
|
||||||
|
metadata["checkpoint"] = checkpoint
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
gen_params.get("prompt")
|
||||||
|
or gen_params.get("positivePrompt")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
name = " ".join(str(prompt).split()[:10]) if prompt else old_title
|
||||||
|
|
||||||
|
result = await self._persistence_service.save_recipe(
|
||||||
|
recipe_scanner=recipe_scanner,
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_base64=analysis_payload.get("image_base64"),
|
||||||
|
name=name,
|
||||||
|
tags=[],
|
||||||
|
metadata=metadata,
|
||||||
|
extension=extension,
|
||||||
|
target_dir=target_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._persistence_service.delete_recipe(
|
||||||
|
recipe_scanner=recipe_scanner, recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
new_recipe_id = result.payload.get("recipe_id")
|
||||||
|
if new_recipe_id and user_edits:
|
||||||
|
try:
|
||||||
|
await self._persistence_service.update_recipe(
|
||||||
|
recipe_scanner=recipe_scanner,
|
||||||
|
recipe_id=new_recipe_id,
|
||||||
|
updates=user_edits,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.warning(
|
||||||
|
"Re-import (local) succeeded but failed to carry over "
|
||||||
|
"user edits for recipe %s: %s",
|
||||||
|
new_recipe_id,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"old_recipe_id": recipe_id,
|
||||||
|
"recipe_id": new_recipe_id,
|
||||||
|
"source_path": normalized,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def create_from_example(self, request: web.Request) -> web.Response:
|
async def create_from_example(self, request: web.Request) -> web.Response:
|
||||||
"""Create a recipe from a model's example image using cached metadata.
|
"""Create a recipe from a model's example image using cached metadata.
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,37 @@ COMMON_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition(
|
RouteDefinition(
|
||||||
"GET", "/api/lm/download-progress/{download_id}", "get_download_progress"
|
"GET", "/api/lm/download-progress/{download_id}", "get_download_progress"
|
||||||
),
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/queue", "get_download_queue"),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/queue/add", "add_to_download_queue"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/remove", "remove_from_download_queue"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/move-to-top", "move_queue_item_to_top"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/move-to-end", "move_queue_item_to_end"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/clear", "clear_download_queue"
|
||||||
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/history", "get_download_history"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/clear", "clear_download_history"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/delete", "delete_download_history_item"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/retry", "retry_download_from_history"
|
||||||
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/history/retry-all", "retry_all_failed_downloads"
|
||||||
|
),
|
||||||
|
RouteDefinition("GET", "/api/lm/downloads/stats", "get_download_stats"),
|
||||||
|
RouteDefinition(
|
||||||
|
"GET", "/api/lm/downloads/queue/complete", "complete_download_in_queue"
|
||||||
|
),
|
||||||
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
||||||
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition(
|
RouteDefinition(
|
||||||
"POST", "/api/lm/recipes/create-from-example", "create_from_example"
|
"POST", "/api/lm/recipes/create-from-example", "create_from_example"
|
||||||
),
|
),
|
||||||
|
RouteDefinition(
|
||||||
|
"POST", "/api/lm/recipe/{recipe_id}/reimport", "reimport_recipe"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import toml
|
import toml
|
||||||
import git
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -225,7 +224,7 @@ class UpdateRoutes:
|
|||||||
logger.debug("Could not close downloaded-version history database", exc_info=True)
|
logger.debug("Could not close downloaded-version history database", exc_info=True)
|
||||||
|
|
||||||
# Skip settings.json, civitai, model cache and runtime cache folders
|
# Skip settings.json, civitai, model cache and runtime cache folders
|
||||||
UpdateRoutes._clean_plugin_folder(plugin_root, skip_files=['settings.json', 'civitai', 'model_cache', 'cache', 'wildcards', 'backups'])
|
UpdateRoutes._clean_plugin_folder(plugin_root, skip_files=['settings.json', 'civitai', 'model_cache', 'cache', 'wildcards', 'backups', 'stats'])
|
||||||
|
|
||||||
# Extract ZIP to temp dir
|
# Extract ZIP to temp dir
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
@@ -235,7 +234,7 @@ class UpdateRoutes:
|
|||||||
extracted_root = next(os.scandir(tmp_dir)).path
|
extracted_root = next(os.scandir(tmp_dir)).path
|
||||||
|
|
||||||
# Copy files, skipping user data that should be preserved
|
# Copy files, skipping user data that should be preserved
|
||||||
skip_items = {'settings.json', 'civitai', 'wildcards', 'backups'}
|
skip_items = {'settings.json', 'civitai', 'wildcards', 'backups', 'stats'}
|
||||||
for item in os.listdir(extracted_root):
|
for item in os.listdir(extracted_root):
|
||||||
if item in skip_items:
|
if item in skip_items:
|
||||||
continue
|
continue
|
||||||
@@ -252,7 +251,7 @@ class UpdateRoutes:
|
|||||||
# for ComfyUI Manager to work properly
|
# for ComfyUI Manager to work properly
|
||||||
tracking_info_file = os.path.join(plugin_root, '.tracking')
|
tracking_info_file = os.path.join(plugin_root, '.tracking')
|
||||||
tracking_files = []
|
tracking_files = []
|
||||||
skip_tracked = {'civitai', 'wildcards', 'backups'}
|
skip_tracked = {'civitai', 'wildcards', 'backups', 'stats'}
|
||||||
for root, dirs, files in os.walk(extracted_root):
|
for root, dirs, files in os.walk(extracted_root):
|
||||||
# Skip user data directories and their contents
|
# Skip user data directories and their contents
|
||||||
rel_root = os.path.relpath(root, extracted_root)
|
rel_root = os.path.relpath(root, extracted_root)
|
||||||
@@ -357,6 +356,15 @@ class UpdateRoutes:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: (success, new_version)
|
tuple: (success, new_version)
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import git
|
||||||
|
except ImportError:
|
||||||
|
logger.error(
|
||||||
|
"GitPython is not available: the git executable was not found in PATH. "
|
||||||
|
"Install git or set $GIT_PYTHON_GIT_EXECUTABLE to the git binary path."
|
||||||
|
)
|
||||||
|
return False, ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open the Git repository
|
# Open the Git repository
|
||||||
repo = git.Repo(plugin_root)
|
repo = git.Repo(plugin_root)
|
||||||
@@ -453,6 +461,7 @@ class UpdateRoutes:
|
|||||||
if not os.path.exists(os.path.join(plugin_root, '.git')):
|
if not os.path.exists(os.path.join(plugin_root, '.git')):
|
||||||
return git_info
|
return git_info
|
||||||
|
|
||||||
|
import git
|
||||||
repo = git.Repo(plugin_root)
|
repo = git.Repo(plugin_root)
|
||||||
commit = repo.head.commit
|
commit = repo.head.commit
|
||||||
git_info['commit_hash'] = commit.hexsha
|
git_info['commit_hash'] = commit.hexsha
|
||||||
|
|||||||
@@ -141,6 +141,16 @@ class BackupService:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stats_path = os.path.join(get_settings_dir(create=True), "stats", "lora_manager_stats.json")
|
||||||
|
if os.path.exists(stats_path):
|
||||||
|
targets.append(
|
||||||
|
(
|
||||||
|
"usage_stats",
|
||||||
|
"stats/lora_manager_stats.json",
|
||||||
|
stats_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -348,6 +358,8 @@ class BackupService:
|
|||||||
if kind == "model_update":
|
if kind == "model_update":
|
||||||
filename = os.path.basename(archive_member)
|
filename = os.path.basename(archive_member)
|
||||||
return str(Path(get_cache_file_path(CacheType.MODEL_UPDATE, create_dir=True)).parent / filename)
|
return str(Path(get_cache_file_path(CacheType.MODEL_UPDATE, create_dir=True)).parent / filename)
|
||||||
|
if kind == "usage_stats":
|
||||||
|
return os.path.join(get_settings_dir(create=True), "stats", "lora_manager_stats.json")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def create_auto_snapshot_if_due(self) -> Optional[dict[str, Any]]:
|
async def create_auto_snapshot_if_due(self) -> Optional[dict[str, Any]]:
|
||||||
|
|||||||
730
py/services/download_queue_service.py
Normal file
730
py/services/download_queue_service.py
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from ..utils.cache_paths import get_cache_base_dir
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_database_path() -> str:
|
||||||
|
base_dir = get_cache_base_dir(create=True)
|
||||||
|
history_dir = os.path.join(base_dir, "download_history")
|
||||||
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
|
return os.path.join(history_dir, "download_queue.sqlite")
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadQueueService:
|
||||||
|
"""Persistent download queue and history manager backed by SQLite.
|
||||||
|
|
||||||
|
Provides a singleton interface for managing a download queue and
|
||||||
|
corresponding history table, both stored in a single SQLite database
|
||||||
|
under the cache directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance: Optional[DownloadQueueService] = None
|
||||||
|
_class_lock: asyncio.Lock = asyncio.Lock()
|
||||||
|
|
||||||
|
_SCHEMA = """
|
||||||
|
CREATE TABLE IF NOT EXISTS download_queue (
|
||||||
|
download_id TEXT PRIMARY KEY,
|
||||||
|
model_id INTEGER,
|
||||||
|
model_version_id INTEGER,
|
||||||
|
model_name TEXT NOT NULL DEFAULT '',
|
||||||
|
version_name TEXT DEFAULT '',
|
||||||
|
thumbnail_url TEXT DEFAULT '',
|
||||||
|
source TEXT,
|
||||||
|
file_params TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'queued',
|
||||||
|
priority INTEGER DEFAULT 0,
|
||||||
|
progress INTEGER DEFAULT 0,
|
||||||
|
bytes_downloaded INTEGER DEFAULT 0,
|
||||||
|
total_bytes INTEGER,
|
||||||
|
bytes_per_second REAL DEFAULT 0.0,
|
||||||
|
error TEXT,
|
||||||
|
file_path TEXT,
|
||||||
|
added_at REAL NOT NULL,
|
||||||
|
started_at REAL,
|
||||||
|
completed_at REAL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dq_status ON download_queue(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dq_added ON download_queue(added_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS download_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
download_id TEXT,
|
||||||
|
model_id INTEGER,
|
||||||
|
model_version_id INTEGER,
|
||||||
|
model_name TEXT NOT NULL DEFAULT '',
|
||||||
|
version_name TEXT DEFAULT '',
|
||||||
|
thumbnail_url TEXT DEFAULT '',
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
error TEXT,
|
||||||
|
file_path TEXT,
|
||||||
|
bytes_downloaded INTEGER DEFAULT 0,
|
||||||
|
total_bytes INTEGER,
|
||||||
|
completed_at REAL NOT NULL,
|
||||||
|
is_already_exists INTEGER DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dh_completed ON download_history(completed_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_dh_status ON download_history(status);
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_instance(cls) -> DownloadQueueService:
|
||||||
|
"""Return the singleton instance, creating it if necessary."""
|
||||||
|
async with cls._class_lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = cls()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, db_path: Optional[str] = None) -> None:
|
||||||
|
self._db_path = db_path or _resolve_database_path()
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._conn: Optional[sqlite3.Connection] = None
|
||||||
|
self._schema_initialized = False
|
||||||
|
self._ensure_directory()
|
||||||
|
self._initialize_schema()
|
||||||
|
|
||||||
|
def _ensure_directory(self) -> None:
|
||||||
|
directory = os.path.dirname(self._db_path)
|
||||||
|
if directory:
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
def _connect(self) -> sqlite3.Connection:
|
||||||
|
conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _get_conn(self) -> sqlite3.Connection:
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
||||||
|
self._conn.row_factory = sqlite3.Row
|
||||||
|
return self._conn
|
||||||
|
|
||||||
|
def _initialize_schema(self) -> None:
|
||||||
|
if self._schema_initialized:
|
||||||
|
return
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.executescript(self._SCHEMA)
|
||||||
|
conn.commit()
|
||||||
|
self._schema_initialized = True
|
||||||
|
|
||||||
|
def get_database_path(self) -> str:
|
||||||
|
"""Return the resolved database file path."""
|
||||||
|
return self._db_path
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the persistent SQLite connection, if open.
|
||||||
|
|
||||||
|
This is called before plugin update operations to release the
|
||||||
|
database file lock on Windows, allowing ``shutil.rmtree()`` to
|
||||||
|
succeed when the cache resides inside the plugin directory.
|
||||||
|
"""
|
||||||
|
if self._conn is not None:
|
||||||
|
try:
|
||||||
|
self._conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Queue methods
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def add_to_queue(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
model_version_id: Optional[int] = None,
|
||||||
|
model_name: str = "",
|
||||||
|
version_name: str = "",
|
||||||
|
thumbnail_url: str = "",
|
||||||
|
source: Optional[str] = None,
|
||||||
|
file_params: Optional[dict[str, Any]] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Insert a new download into the queue.
|
||||||
|
|
||||||
|
Returns the inserted row as a dict (or an empty dict if the
|
||||||
|
download_id already exists).
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
file_params_json = json.dumps(file_params) if file_params is not None else None
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT OR IGNORE INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
download_id,
|
||||||
|
model_id,
|
||||||
|
model_version_id,
|
||||||
|
model_name,
|
||||||
|
version_name,
|
||||||
|
thumbnail_url,
|
||||||
|
source,
|
||||||
|
file_params_json,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(row) if row else {}
|
||||||
|
|
||||||
|
async def get_queue(self) -> list[dict[str, Any]]:
|
||||||
|
"""Return all items in the queue ordered by priority then added time."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_queue ORDER BY priority DESC, added_at ASC"
|
||||||
|
).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
async def get_queued_count(self) -> int:
|
||||||
|
"""Return the number of items with status ``'queued'``."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_queue WHERE status = 'queued'"
|
||||||
|
).fetchone()
|
||||||
|
return row["cnt"] if row else 0
|
||||||
|
|
||||||
|
async def update_status(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
status: str,
|
||||||
|
**extra: Any,
|
||||||
|
) -> bool:
|
||||||
|
"""Update the status and/or extra fields of a queue item.
|
||||||
|
|
||||||
|
Accepted extra keyword arguments:
|
||||||
|
``progress``, ``error``, ``file_path``, ``bytes_downloaded``,
|
||||||
|
``total_bytes``, ``bytes_per_second``.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was updated.
|
||||||
|
"""
|
||||||
|
allowed_extra = {
|
||||||
|
"progress",
|
||||||
|
"error",
|
||||||
|
"file_path",
|
||||||
|
"bytes_downloaded",
|
||||||
|
"total_bytes",
|
||||||
|
"bytes_per_second",
|
||||||
|
}
|
||||||
|
|
||||||
|
set_clauses: list[str] = ["status = ?"]
|
||||||
|
params: list[Any] = [status]
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if status in ("downloading",):
|
||||||
|
set_clauses.append("started_at = COALESCE(started_at, ?)")
|
||||||
|
params.append(now)
|
||||||
|
if status in ("completed", "failed", "canceled"):
|
||||||
|
set_clauses.append("completed_at = ?")
|
||||||
|
params.append(now)
|
||||||
|
|
||||||
|
for key, value in extra.items():
|
||||||
|
if key in allowed_extra:
|
||||||
|
set_clauses.append(f"{key} = ?")
|
||||||
|
params.append(value)
|
||||||
|
|
||||||
|
params.append(download_id)
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
f"UPDATE download_queue SET {', '.join(set_clauses)} "
|
||||||
|
"WHERE download_id = ?",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def remove_from_queue(self, download_id: str) -> bool:
|
||||||
|
"""Remove a single item from the queue by download_id.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was deleted.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def move_to_top(self, download_id: str) -> bool:
|
||||||
|
"""Move an item to the front of the queue (highest priority).
|
||||||
|
|
||||||
|
Returns ``True`` if the item was found and updated.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT priority FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
max_row = conn.execute(
|
||||||
|
"SELECT MAX(priority) AS mx FROM download_queue"
|
||||||
|
).fetchone()
|
||||||
|
max_priority: int = max_row["mx"] if max_row["mx"] is not None else 0
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET priority = ? WHERE download_id = ?",
|
||||||
|
(max_priority + 1, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def move_to_end(self, download_id: str) -> bool:
|
||||||
|
"""Move an item to the end of the queue (lowest priority).
|
||||||
|
|
||||||
|
Returns ``True`` if the item was found and updated.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT priority FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
min_row = conn.execute(
|
||||||
|
"SELECT MIN(priority) AS mn FROM download_queue"
|
||||||
|
).fetchone()
|
||||||
|
min_priority: int = min_row["mn"] if min_row["mn"] is not None else 0
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET priority = ? WHERE download_id = ?",
|
||||||
|
(min_priority - 1, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def clear_queue(self, status_filter: Optional[str] = None) -> int:
|
||||||
|
"""Remove items from the queue.
|
||||||
|
|
||||||
|
When *status_filter* is provided only items with that status are
|
||||||
|
deleted. Returns the number of deleted rows.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
if status_filter is not None:
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cursor = conn.execute("DELETE FROM download_queue")
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
async def complete_download(
|
||||||
|
self,
|
||||||
|
download_id: str,
|
||||||
|
status: str = "completed",
|
||||||
|
error: Optional[str] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
bytes_downloaded: int = 0,
|
||||||
|
total_bytes: Optional[int] = None,
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""Atomically move a download from the queue into the history table.
|
||||||
|
|
||||||
|
Looks up the queue record by ``download_id``, deletes it from the
|
||||||
|
queue, and inserts a corresponding history entry with the given
|
||||||
|
terminal status (``completed``, ``failed``, or ``canceled``).
|
||||||
|
|
||||||
|
Returns the original queue record (before deletion) on success,
|
||||||
|
or ``None`` if the download was not found in the queue.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_history (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, status, error, file_path,
|
||||||
|
bytes_downloaded, total_bytes, completed_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
row["download_id"],
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
file_path,
|
||||||
|
bytes_downloaded,
|
||||||
|
total_bytes,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
async def pop_next_download(self) -> Optional[dict[str, Any]]:
|
||||||
|
"""Atomically fetch and mark the next queued item as ``downloading``.
|
||||||
|
|
||||||
|
The item with the highest priority (and earliest ``added_at``
|
||||||
|
among ties) whose status is ``'queued'`` is selected, set to
|
||||||
|
``'downloading'``, and returned as a dict. Returns ``None`` if
|
||||||
|
the queue is empty.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT * FROM download_queue
|
||||||
|
WHERE status = 'queued'
|
||||||
|
ORDER BY priority DESC, added_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
download_id = row["download_id"]
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE download_queue SET status = 'downloading', "
|
||||||
|
"started_at = COALESCE(started_at, ?) "
|
||||||
|
"WHERE download_id = ?",
|
||||||
|
(now, download_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
updated = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(download_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(updated) if updated else None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# History methods
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def add_to_history(
|
||||||
|
self,
|
||||||
|
download_id: Optional[str] = None,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
model_version_id: Optional[int] = None,
|
||||||
|
model_name: str = "",
|
||||||
|
version_name: str = "",
|
||||||
|
thumbnail_url: str = "",
|
||||||
|
status: str = "completed",
|
||||||
|
error: Optional[str] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
bytes_downloaded: int = 0,
|
||||||
|
total_bytes: Optional[int] = None,
|
||||||
|
is_already_exists: int = 0,
|
||||||
|
) -> int:
|
||||||
|
"""Insert a record into the download history.
|
||||||
|
|
||||||
|
Returns the ``id`` (AUTOINCREMENT primary key) of the newly
|
||||||
|
inserted row.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_history (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, status, error, file_path,
|
||||||
|
bytes_downloaded, total_bytes, completed_at, is_already_exists
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
download_id,
|
||||||
|
model_id,
|
||||||
|
model_version_id,
|
||||||
|
model_name,
|
||||||
|
version_name,
|
||||||
|
thumbnail_url,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
file_path,
|
||||||
|
bytes_downloaded,
|
||||||
|
total_bytes,
|
||||||
|
now,
|
||||||
|
is_already_exists,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.lastrowid or 0
|
||||||
|
|
||||||
|
async def get_history(
|
||||||
|
self,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return a page of download history entries.
|
||||||
|
|
||||||
|
Returns a dict with keys ``items``, ``total``, ``limit``, and
|
||||||
|
``offset``.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
if status_filter is not None:
|
||||||
|
count_row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
).fetchone()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE status = ? "
|
||||||
|
"ORDER BY completed_at DESC LIMIT ? OFFSET ?",
|
||||||
|
(status_filter, limit, offset),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
count_row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history"
|
||||||
|
).fetchone()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history "
|
||||||
|
"ORDER BY completed_at DESC LIMIT ? OFFSET ?",
|
||||||
|
(limit, offset),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": [dict(row) for row in rows],
|
||||||
|
"total": count_row["cnt"] if count_row else 0,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def delete_history_item(self, id: int) -> bool:
|
||||||
|
"""Delete a single history entry by its *id*.
|
||||||
|
|
||||||
|
Returns ``True`` if a row was deleted.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
cursor = conn.execute(
|
||||||
|
"DELETE FROM download_history WHERE id = ?",
|
||||||
|
(id,),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
|
async def clear_history(
|
||||||
|
self,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
before_timestamp: Optional[float] = None,
|
||||||
|
) -> int:
|
||||||
|
"""Remove history entries matching the optional filters.
|
||||||
|
|
||||||
|
Both ``status_filter`` and ``before_timestamp`` can be combined
|
||||||
|
(AND logic). Returns the number of deleted rows.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
clauses: list[str] = []
|
||||||
|
params: list[Any] = []
|
||||||
|
|
||||||
|
if status_filter is not None:
|
||||||
|
clauses.append("status = ?")
|
||||||
|
params.append(status_filter)
|
||||||
|
if before_timestamp is not None:
|
||||||
|
clauses.append("completed_at < ?")
|
||||||
|
params.append(before_timestamp)
|
||||||
|
|
||||||
|
where = ""
|
||||||
|
if clauses:
|
||||||
|
where = " WHERE " + " AND ".join(clauses)
|
||||||
|
|
||||||
|
cursor = conn.execute(
|
||||||
|
f"DELETE FROM download_history{where}",
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount
|
||||||
|
|
||||||
|
async def get_history_count(self, status_filter: Optional[str] = None) -> int:
|
||||||
|
"""Return the number of history entries, optionally filtered by status."""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
if status_filter is not None:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history WHERE status = ?",
|
||||||
|
(status_filter,),
|
||||||
|
).fetchone()
|
||||||
|
else:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT COUNT(*) AS cnt FROM download_history"
|
||||||
|
).fetchone()
|
||||||
|
return row["cnt"] if row else 0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Retry
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def retry_from_history(self, item_id: int) -> Optional[dict[str, Any]]:
|
||||||
|
"""Re-queue a failed or canceled download from history.
|
||||||
|
|
||||||
|
Looks up the history record by its primary key. If the status is
|
||||||
|
``failed`` or ``canceled`` a new queue entry is created with the
|
||||||
|
same model metadata and a fresh download id.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE id = ?",
|
||||||
|
(item_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
status = str(row["status"])
|
||||||
|
if status not in ("failed", "canceled"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
|
now = time.time()
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_id,
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
"retry",
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
queued = conn.execute(
|
||||||
|
"SELECT * FROM download_queue WHERE download_id = ?",
|
||||||
|
(new_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
return dict(queued) if queued else None
|
||||||
|
|
||||||
|
async def retry_all_failed(self) -> int:
|
||||||
|
"""Re-queue all failed and canceled downloads from history.
|
||||||
|
|
||||||
|
Returns the number of items that were re-queued.
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM download_history WHERE status IN ('failed', 'canceled')"
|
||||||
|
).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
count = 0
|
||||||
|
for row in rows:
|
||||||
|
new_id = str(uuid.uuid4())
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO download_queue (
|
||||||
|
download_id, model_id, model_version_id, model_name,
|
||||||
|
version_name, thumbnail_url, source, file_params,
|
||||||
|
status, priority, added_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'queued', 0, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
new_id,
|
||||||
|
row["model_id"],
|
||||||
|
row["model_version_id"],
|
||||||
|
row["model_name"],
|
||||||
|
row["version_name"],
|
||||||
|
row["thumbnail_url"],
|
||||||
|
"retry",
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Stats
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def get_stats(self) -> dict[str, int]:
|
||||||
|
"""Return aggregate counts across both tables.
|
||||||
|
|
||||||
|
Returns a dict with keys ``queued``, ``downloading``, ``paused``
|
||||||
|
(all from the queue table) and ``completed``, ``failed``,
|
||||||
|
``canceled`` (all from the history table).
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
conn = self._get_conn()
|
||||||
|
|
||||||
|
queue_rows = conn.execute(
|
||||||
|
"SELECT status, COUNT(*) AS cnt FROM download_queue GROUP BY status"
|
||||||
|
).fetchall()
|
||||||
|
queue_stats: dict[str, int] = {}
|
||||||
|
for row in queue_rows:
|
||||||
|
queue_stats[str(row["status"])] = row["cnt"]
|
||||||
|
|
||||||
|
history_rows = conn.execute(
|
||||||
|
"SELECT status, COUNT(*) AS cnt FROM download_history GROUP BY status"
|
||||||
|
).fetchall()
|
||||||
|
history_stats: dict[str, int] = {}
|
||||||
|
for row in history_rows:
|
||||||
|
history_stats[str(row["status"])] = row["cnt"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"queued": queue_stats.get("queued", 0),
|
||||||
|
"downloading": queue_stats.get("downloading", 0),
|
||||||
|
"paused": queue_stats.get("paused", 0),
|
||||||
|
"completed": history_stats.get("completed", 0),
|
||||||
|
"failed": history_stats.get("failed", 0),
|
||||||
|
"canceled": history_stats.get("canceled", 0),
|
||||||
|
}
|
||||||
@@ -176,6 +176,24 @@ class RecipeAnalysisService:
|
|||||||
self._exif_utils.extract_image_metadata, temp_path
|
self._exif_utils.extract_image_metadata, temp_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not metadata and civitai_image_id and image_info:
|
||||||
|
original_url = image_info.get("url")
|
||||||
|
if original_url:
|
||||||
|
self._logger.debug(
|
||||||
|
"Optimized image lacks embedded metadata, "
|
||||||
|
"falling back to original image: %s",
|
||||||
|
original_url,
|
||||||
|
)
|
||||||
|
orig_temp_path = self._create_temp_path(suffix=".png")
|
||||||
|
try:
|
||||||
|
await self._download_image(original_url, orig_temp_path)
|
||||||
|
metadata = await asyncio.to_thread(
|
||||||
|
self._exif_utils.extract_image_metadata,
|
||||||
|
orig_temp_path,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._safe_cleanup(orig_temp_path)
|
||||||
|
|
||||||
result = await self._parse_metadata(
|
result = await self._parse_metadata(
|
||||||
metadata or {},
|
metadata or {},
|
||||||
recipe_scanner=recipe_scanner,
|
recipe_scanner=recipe_scanner,
|
||||||
|
|||||||
@@ -49,8 +49,18 @@ class RecipePersistenceService:
|
|||||||
tags: Iterable[str],
|
tags: Iterable[str],
|
||||||
metadata: Optional[dict[str, Any]],
|
metadata: Optional[dict[str, Any]],
|
||||||
extension: str | None = None,
|
extension: str | None = None,
|
||||||
|
recipe_id: str | None = None,
|
||||||
|
target_dir: str | None = None,
|
||||||
) -> PersistenceResult:
|
) -> PersistenceResult:
|
||||||
"""Persist a user uploaded recipe."""
|
"""Persist a user uploaded recipe.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipe_id: If provided, reuse this ID instead of generating a new
|
||||||
|
UUID. Used by re-import to preserve the original recipe identity.
|
||||||
|
target_dir: If provided, save recipe files to this directory instead
|
||||||
|
of the default recipes_dir. Used by re-import to preserve the
|
||||||
|
original folder location.
|
||||||
|
"""
|
||||||
|
|
||||||
missing_fields = []
|
missing_fields = []
|
||||||
if not name:
|
if not name:
|
||||||
@@ -63,10 +73,10 @@ class RecipePersistenceService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
resolved_image_bytes = self._resolve_image_bytes(image_bytes, image_base64)
|
resolved_image_bytes = self._resolve_image_bytes(image_bytes, image_base64)
|
||||||
recipes_dir = recipe_scanner.recipes_dir
|
recipes_dir = target_dir or recipe_scanner.recipes_dir
|
||||||
os.makedirs(recipes_dir, exist_ok=True)
|
os.makedirs(recipes_dir, exist_ok=True)
|
||||||
|
|
||||||
recipe_id = str(uuid.uuid4())
|
recipe_id = recipe_id or str(uuid.uuid4())
|
||||||
|
|
||||||
# Handle video formats by bypassing optimization and metadata embedding
|
# Handle video formats by bypassing optimization and metadata embedding
|
||||||
is_video = extension in [".mp4", ".webm"]
|
is_video = extension in [".mp4", ".webm"]
|
||||||
@@ -119,6 +129,18 @@ class RecipePersistenceService:
|
|||||||
if nsfw_level is not None and isinstance(nsfw_level, int):
|
if nsfw_level is not None and isinstance(nsfw_level, int):
|
||||||
recipe_data["preview_nsfw_level"] = nsfw_level
|
recipe_data["preview_nsfw_level"] = nsfw_level
|
||||||
|
|
||||||
|
# Compute recipe folder relative to recipes root, mirroring
|
||||||
|
# RecipeScanner._calculate_folder() which is only called during scan/load.
|
||||||
|
if recipe_scanner.recipes_dir:
|
||||||
|
recipe_file_dir = os.path.dirname(normalized_image_path)
|
||||||
|
try:
|
||||||
|
relative_folder = os.path.relpath(recipe_file_dir, recipe_scanner.recipes_dir)
|
||||||
|
if relative_folder in (".", ""):
|
||||||
|
relative_folder = ""
|
||||||
|
recipe_data["folder"] = relative_folder.replace(os.path.sep, "/")
|
||||||
|
except Exception:
|
||||||
|
recipe_data["folder"] = ""
|
||||||
|
|
||||||
json_filename = f"{recipe_id}.recipe.json"
|
json_filename = f"{recipe_id}.recipe.json"
|
||||||
json_path = os.path.join(recipes_dir, json_filename)
|
json_path = os.path.join(recipes_dir, json_filename)
|
||||||
json_path = os.path.normpath(json_path)
|
json_path = os.path.normpath(json_path)
|
||||||
|
|||||||
@@ -188,6 +188,25 @@ class ServiceRegistry:
|
|||||||
logger.debug(f"Created and registered {service_name}")
|
logger.debug(f"Created and registered {service_name}")
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_download_queue_service(cls):
|
||||||
|
"""Get or create the download queue service."""
|
||||||
|
service_name = "download_queue_service"
|
||||||
|
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
async with cls._get_lock(service_name):
|
||||||
|
if service_name in cls._services:
|
||||||
|
return cls._services[service_name]
|
||||||
|
|
||||||
|
from .download_queue_service import DownloadQueueService
|
||||||
|
|
||||||
|
service = await DownloadQueueService.get_instance()
|
||||||
|
cls._services[service_name] = service
|
||||||
|
logger.debug(f"Created and registered {service_name}")
|
||||||
|
return service
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_backup_service(cls):
|
async def get_backup_service(cls):
|
||||||
"""Get or create the backup service."""
|
"""Get or create the backup service."""
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def read_safetensors_metadata(file_path: str) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
header = json.loads(header_bytes.decode("utf-8"))
|
header = json.loads(header_bytes.decode("utf-8"))
|
||||||
return header.get("__metadata__", {})
|
return header.get("__metadata__", {})
|
||||||
except (OSError, json.JSONDecodeError, UnicodeDecodeError, struct.error):
|
except (OSError, json.JSONDecodeError, UnicodeDecodeError, struct.error, MemoryError, Exception):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -9,6 +10,7 @@ from typing import Dict, Set
|
|||||||
|
|
||||||
from ..config import config
|
from ..config import config
|
||||||
from ..services.service_registry import ServiceRegistry
|
from ..services.service_registry import ServiceRegistry
|
||||||
|
from ..utils.settings_paths import get_settings_dir
|
||||||
|
|
||||||
# Check if running in standalone mode
|
# Check if running in standalone mode
|
||||||
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1" or os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0"
|
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1" or os.environ.get("HF_HUB_DISABLE_TELEMETRY", "0") == "0"
|
||||||
@@ -16,14 +18,18 @@ standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1" or os.en
|
|||||||
# Define constants locally to avoid dependency on conditional imports
|
# Define constants locally to avoid dependency on conditional imports
|
||||||
MODELS = "models"
|
MODELS = "models"
|
||||||
LORAS = "loras"
|
LORAS = "loras"
|
||||||
|
EMBEDDINGS = "embeddings"
|
||||||
|
PROMPTS = "prompts"
|
||||||
|
|
||||||
if not standalone_mode:
|
if not standalone_mode:
|
||||||
from ..metadata_collector.metadata_registry import MetadataRegistry
|
from ..metadata_collector.metadata_registry import MetadataRegistry
|
||||||
# Import constants from metadata_collector to ensure consistency, but we have fallbacks defined above
|
# Import constants from metadata_collector to ensure consistency, but we have fallbacks defined above
|
||||||
try:
|
try:
|
||||||
from ..metadata_collector.constants import MODELS as _MODELS, LORAS as _LORAS
|
from ..metadata_collector.constants import MODELS as _MODELS, LORAS as _LORAS, EMBEDDINGS as _EMBEDDINGS, PROMPTS as _PROMPTS
|
||||||
MODELS = _MODELS
|
MODELS = _MODELS
|
||||||
LORAS = _LORAS
|
LORAS = _LORAS
|
||||||
|
EMBEDDINGS = _EMBEDDINGS
|
||||||
|
PROMPTS = _PROMPTS
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass # Use the local definitions
|
pass # Use the local definitions
|
||||||
|
|
||||||
@@ -65,6 +71,7 @@ class UsageStats:
|
|||||||
self.stats = {
|
self.stats = {
|
||||||
"checkpoints": {}, # sha256 -> { total: count, history: { date: count } }
|
"checkpoints": {}, # sha256 -> { total: count, history: { date: count } }
|
||||||
"loras": {}, # sha256 -> { total: count, history: { date: count } }
|
"loras": {}, # sha256 -> { total: count, history: { date: count } }
|
||||||
|
"embeddings": {}, # sha256 -> { total: count, history: { date: count } }
|
||||||
"total_executions": 0,
|
"total_executions": 0,
|
||||||
"last_save_time": 0
|
"last_save_time": 0
|
||||||
}
|
}
|
||||||
@@ -77,6 +84,7 @@ class UsageStats:
|
|||||||
|
|
||||||
# Load existing stats if available
|
# Load existing stats if available
|
||||||
self._stats_file_path = self._get_stats_file_path()
|
self._stats_file_path = self._get_stats_file_path()
|
||||||
|
self._migrate_from_old_location()
|
||||||
self._load_stats()
|
self._load_stats()
|
||||||
|
|
||||||
# Save interval in seconds
|
# Save interval in seconds
|
||||||
@@ -89,14 +97,38 @@ class UsageStats:
|
|||||||
logger.debug("Usage statistics tracker initialized")
|
logger.debug("Usage statistics tracker initialized")
|
||||||
|
|
||||||
def _get_stats_file_path(self) -> str:
|
def _get_stats_file_path(self) -> str:
|
||||||
"""Get the path to the stats JSON file"""
|
"""Get the path to the stats JSON file in the settings directory."""
|
||||||
if not config.loras_roots or len(config.loras_roots) == 0:
|
settings_dir = get_settings_dir(create=True)
|
||||||
# If no lora roots are available, we can't save stats
|
return os.path.join(settings_dir, "stats", self.STATS_FILENAME)
|
||||||
# This will be handled by the caller
|
|
||||||
raise RuntimeError("No LoRA root directories configured. Cannot initialize usage statistics.")
|
|
||||||
|
|
||||||
# Use the first lora root
|
@staticmethod
|
||||||
return os.path.join(config.loras_roots[0], self.STATS_FILENAME)
|
def _get_old_stats_file_path() -> str:
|
||||||
|
"""Get the legacy stats file path in the first lora root directory."""
|
||||||
|
if not config.loras_roots or len(config.loras_roots) == 0:
|
||||||
|
return ""
|
||||||
|
return os.path.join(config.loras_roots[0], UsageStats.STATS_FILENAME)
|
||||||
|
|
||||||
|
def _migrate_from_old_location(self) -> None:
|
||||||
|
"""Migrate stats file from old location (first lora root) to new location (settings_dir/stats/)."""
|
||||||
|
new_path = self._stats_file_path
|
||||||
|
if os.path.exists(new_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
old_path = self._get_old_stats_file_path()
|
||||||
|
if not old_path or not os.path.exists(old_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||||
|
shutil.copy2(old_path, new_path)
|
||||||
|
logger.info("Migrated usage stats from %s to %s", old_path, new_path)
|
||||||
|
try:
|
||||||
|
os.remove(old_path)
|
||||||
|
logger.info("Cleaned up old stats file: %s", old_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to remove old stats file %s: %s", old_path, e)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to migrate usage stats from %s to %s: %s", old_path, new_path, e)
|
||||||
|
|
||||||
def _backup_old_stats(self):
|
def _backup_old_stats(self):
|
||||||
"""Backup the old stats file before conversion"""
|
"""Backup the old stats file before conversion"""
|
||||||
@@ -115,6 +147,7 @@ class UsageStats:
|
|||||||
new_stats = {
|
new_stats = {
|
||||||
"checkpoints": {},
|
"checkpoints": {},
|
||||||
"loras": {},
|
"loras": {},
|
||||||
|
"embeddings": {},
|
||||||
"total_executions": old_stats.get("total_executions", 0),
|
"total_executions": old_stats.get("total_executions", 0),
|
||||||
"last_save_time": old_stats.get("last_save_time", time.time())
|
"last_save_time": old_stats.get("last_save_time", time.time())
|
||||||
}
|
}
|
||||||
@@ -142,19 +175,25 @@ class UsageStats:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Convert embedding stats (if present in old format)
|
||||||
|
if "embeddings" in old_stats and isinstance(old_stats["embeddings"], dict):
|
||||||
|
for hash_id, count in old_stats["embeddings"].items():
|
||||||
|
new_stats["embeddings"][hash_id] = {
|
||||||
|
"total": count,
|
||||||
|
"history": {
|
||||||
|
today: count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Successfully converted stats from old format to new format with history")
|
logger.info("Successfully converted stats from old format to new format with history")
|
||||||
return new_stats
|
return new_stats
|
||||||
|
|
||||||
def _is_old_format(self, stats):
|
def _is_old_format(self, stats):
|
||||||
"""Check if the stats are in the old format (direct count values)"""
|
"""Check if the stats are in the old format (direct count values)"""
|
||||||
# Check if any lora or checkpoint entry is a direct number instead of an object
|
# Check if any lora or checkpoint entry is a direct number instead of an object
|
||||||
if "loras" in stats and isinstance(stats["loras"], dict):
|
for category in ("loras", "checkpoints", "embeddings"):
|
||||||
for hash_id, data in stats["loras"].items():
|
if category in stats and isinstance(stats[category], dict):
|
||||||
if isinstance(data, (int, float)):
|
for hash_id, data in stats[category].items():
|
||||||
return True
|
|
||||||
|
|
||||||
if "checkpoints" in stats and isinstance(stats["checkpoints"], dict):
|
|
||||||
for hash_id, data in stats["checkpoints"].items():
|
|
||||||
if isinstance(data, (int, float)):
|
if isinstance(data, (int, float)):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -182,6 +221,9 @@ class UsageStats:
|
|||||||
if "loras" in loaded_stats and isinstance(loaded_stats["loras"], dict):
|
if "loras" in loaded_stats and isinstance(loaded_stats["loras"], dict):
|
||||||
self.stats["loras"] = loaded_stats["loras"]
|
self.stats["loras"] = loaded_stats["loras"]
|
||||||
|
|
||||||
|
if "embeddings" in loaded_stats and isinstance(loaded_stats["embeddings"], dict):
|
||||||
|
self.stats["embeddings"] = loaded_stats["embeddings"]
|
||||||
|
|
||||||
if "total_executions" in loaded_stats:
|
if "total_executions" in loaded_stats:
|
||||||
self.stats["total_executions"] = loaded_stats["total_executions"]
|
self.stats["total_executions"] = loaded_stats["total_executions"]
|
||||||
|
|
||||||
@@ -304,6 +346,10 @@ class UsageStats:
|
|||||||
if LORAS in metadata and isinstance(metadata[LORAS], dict):
|
if LORAS in metadata and isinstance(metadata[LORAS], dict):
|
||||||
await self._process_loras(metadata[LORAS], today)
|
await self._process_loras(metadata[LORAS], today)
|
||||||
|
|
||||||
|
# Process embeddings — parse prompt text for embedding:name references
|
||||||
|
if PROMPTS in metadata and isinstance(metadata[PROMPTS], dict):
|
||||||
|
await self._process_embeddings(metadata[PROMPTS], today)
|
||||||
|
|
||||||
def _increment_usage_counter(self, category: str, stat_key: str, today_date: str) -> None:
|
def _increment_usage_counter(self, category: str, stat_key: str, today_date: str) -> None:
|
||||||
"""Increment usage counters for a resolved stats key."""
|
"""Increment usage counters for a resolved stats key."""
|
||||||
if stat_key not in self.stats[category]:
|
if stat_key not in self.stats[category]:
|
||||||
@@ -510,6 +556,55 @@ class UsageStats:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing LoRA usage: {e}", exc_info=True)
|
logger.error(f"Error processing LoRA usage: {e}", exc_info=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_embedding_names(prompt_text: str) -> set:
|
||||||
|
"""Parse embedding:name references from prompt text.
|
||||||
|
|
||||||
|
ComfyUI's SDTokenizer resolves ``embedding:<name>`` during tokenization
|
||||||
|
(see ``sd1_clip.py _try_get_embedding``). This mirrors the same pattern
|
||||||
|
to extract embedding file names from the captured prompt strings.
|
||||||
|
"""
|
||||||
|
if not prompt_text:
|
||||||
|
return set()
|
||||||
|
# Matches ``embedding:name`` where name is alphanumeric plus _ . - /
|
||||||
|
names = re.findall(r"embedding:([a-zA-Z0-9_.\-/]+)", prompt_text)
|
||||||
|
return set(names)
|
||||||
|
|
||||||
|
async def _process_embeddings(self, prompts_data, today_date):
|
||||||
|
"""Extract embedding usage from prompt texts and record it.
|
||||||
|
|
||||||
|
Iterates every prompt node's text field captured by the metadata
|
||||||
|
collector, extracts ``embedding:<name>`` references, resolves each
|
||||||
|
name to its SHA256 hash via the embedding scanner, and increments
|
||||||
|
usage counters.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
|
||||||
|
if not embedding_scanner:
|
||||||
|
logger.warning("Embedding scanner not available for usage tracking")
|
||||||
|
return
|
||||||
|
|
||||||
|
seen_names = set()
|
||||||
|
for _node_id, prompt_data in prompts_data.items():
|
||||||
|
if not isinstance(prompt_data, dict):
|
||||||
|
continue
|
||||||
|
for text_field in ("text", "positive_text", "negative_text"):
|
||||||
|
text = prompt_data.get(text_field)
|
||||||
|
if isinstance(text, str):
|
||||||
|
seen_names.update(self._extract_embedding_names(text))
|
||||||
|
|
||||||
|
for emb_name in seen_names:
|
||||||
|
emb_hash = embedding_scanner.get_hash_by_filename(emb_name)
|
||||||
|
if emb_hash:
|
||||||
|
self._increment_usage_counter("embeddings", emb_hash, today_date)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
"No hash found for embedding '%s', skipping usage tracking",
|
||||||
|
emb_name,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error processing embedding usage: %s", e, exc_info=True)
|
||||||
|
|
||||||
async def get_stats(self):
|
async def get_stats(self):
|
||||||
"""Get current usage statistics"""
|
"""Get current usage statistics"""
|
||||||
return self.stats
|
return self.stats
|
||||||
@@ -522,6 +617,9 @@ class UsageStats:
|
|||||||
elif model_type == "lora":
|
elif model_type == "lora":
|
||||||
if sha256 in self.stats["loras"]:
|
if sha256 in self.stats["loras"]:
|
||||||
return self.stats["loras"][sha256]["total"]
|
return self.stats["loras"][sha256]["total"]
|
||||||
|
elif model_type == "embedding":
|
||||||
|
if sha256 in self.stats["embeddings"]:
|
||||||
|
return self.stats["embeddings"][sha256]["total"]
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
async def process_execution(self, prompt_id):
|
async def process_execution(self, prompt_id):
|
||||||
|
|||||||
@@ -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.0.11"
|
version = "1.1.0"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
@@ -53,10 +55,7 @@ def resolve_settings_path() -> Path:
|
|||||||
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
||||||
return portable
|
return portable
|
||||||
|
|
||||||
config_home = os.environ.get("XDG_CONFIG_HOME")
|
return Path(user_config_dir(APP_NAME, appauthor=False)) / "settings.json"
|
||||||
if config_home:
|
|
||||||
return Path(config_home).expanduser() / APP_NAME / "settings.json"
|
|
||||||
return Path.home() / ".config" / APP_NAME / "settings.json"
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> dict[str, Any]:
|
def load_json(path: Path) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(message)s",
|
format="%(message)s",
|
||||||
@@ -68,10 +70,7 @@ def resolve_settings_path() -> Path:
|
|||||||
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
||||||
return portable
|
return portable
|
||||||
|
|
||||||
config_home = os.environ.get("XDG_CONFIG_HOME")
|
return Path(user_config_dir(APP_NAME, appauthor=False)) / "settings.json"
|
||||||
if config_home:
|
|
||||||
return Path(config_home).expanduser() / APP_NAME / "settings.json"
|
|
||||||
return Path.home() / ".config" / APP_NAME / "settings.json"
|
|
||||||
|
|
||||||
|
|
||||||
def _load_json(path: Path) -> dict[str, Any]:
|
def _load_json(path: Path) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
|
@import 'tokens/index.css';
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* Disable default scrolling */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 针对Firefox */
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--border-color) transparent;
|
scrollbar-color: var(--border-base) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 针对Webkit browsers (Chrome, Safari等) */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: var(--scrollbar-width, 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -24,116 +23,128 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-base);
|
||||||
border-radius: 4px;
|
border-radius: var(--radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #ffffff;
|
|
||||||
--text-color: #333333;
|
|
||||||
--text-muted: #6c757d;
|
|
||||||
--card-bg: #ffffff;
|
|
||||||
--border-color: #e0e0e0;
|
|
||||||
--header-height: 48px;
|
--header-height: 48px;
|
||||||
|
|
||||||
/* Color Components */
|
|
||||||
--lora-accent-l: 68%;
|
|
||||||
--lora-accent-c: 0.28;
|
|
||||||
--lora-accent-h: 256;
|
|
||||||
--lora-warning-l: 75%;
|
|
||||||
--lora-warning-c: 0.25;
|
|
||||||
--lora-warning-h: 80;
|
|
||||||
--lora-success-l: 70%;
|
|
||||||
--lora-success-c: 0.2;
|
|
||||||
--lora-success-h: 140;
|
|
||||||
|
|
||||||
/* Composed Colors */
|
|
||||||
--lora-accent: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
|
|
||||||
--lora-surface: oklch(97% 0 0 / 0.95);
|
|
||||||
--lora-border: oklch(72% 0.03 256 / 0.45);
|
|
||||||
--lora-text: oklch(95% 0.02 256);
|
|
||||||
--lora-error: oklch(75% 0.32 29);
|
|
||||||
--lora-error-bg: color-mix(in oklch, var(--lora-error) 20%, transparent);
|
|
||||||
--lora-error-border: color-mix(in oklch, var(--lora-error) 50%, transparent);
|
|
||||||
--lora-warning: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
|
|
||||||
--lora-success: oklch(var(--lora-success-l) var(--lora-success-c) var(--lora-success-h));
|
|
||||||
--badge-update-bg: oklch(72% 0.2 220);
|
|
||||||
--badge-update-text: oklch(28% 0.03 220);
|
|
||||||
--badge-update-glow: oklch(72% 0.2 220 / 0.28);
|
|
||||||
--badge-skip-refresh-bg: oklch(82% 0.12 45);
|
|
||||||
--badge-skip-refresh-text: oklch(35% 0.02 45);
|
|
||||||
--badge-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
|
|
||||||
|
|
||||||
/* Spacing Scale */
|
|
||||||
--space-1: calc(8px * 1);
|
|
||||||
--space-2: calc(8px * 2);
|
|
||||||
--space-3: calc(8px * 3);
|
|
||||||
--space-4: calc(8px * 4);
|
|
||||||
|
|
||||||
/* Z-index Scale */
|
|
||||||
--z-base: 10;
|
|
||||||
--z-header: 100;
|
|
||||||
--z-modal: 1000;
|
|
||||||
--z-overlay: 2000;
|
|
||||||
|
|
||||||
/* Border Radius */
|
|
||||||
--border-radius-base: 12px;
|
|
||||||
--border-radius-md: 12px;
|
|
||||||
--border-radius-sm: 8px;
|
|
||||||
--border-radius-xs: 4px;
|
|
||||||
|
|
||||||
--scrollbar-width: 8px;
|
--scrollbar-width: 8px;
|
||||||
/* 添加滚动条宽度变量 */
|
|
||||||
|
|
||||||
/* Shortcut styles */
|
--shortcut-bg: var(--color-accent-subtle);
|
||||||
--shortcut-bg: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.12);
|
--shortcut-border: var(--color-accent-border);
|
||||||
--shortcut-border: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.25);
|
--shortcut-text: var(--text-primary);
|
||||||
--shortcut-text: var(--text-color);
|
|
||||||
|
--lora-accent-transparent: var(--color-accent-transparent);
|
||||||
|
|
||||||
|
/* Legacy spacing aliases: 8px base grid to match existing component usage */
|
||||||
|
--space-1: 8px;
|
||||||
|
--space-2: 16px;
|
||||||
|
--space-3: 24px;
|
||||||
|
--space-4: 32px;
|
||||||
|
|
||||||
|
/* Legacy border-radius aliases to match existing component usage */
|
||||||
|
--border-radius-xs: 4px;
|
||||||
|
--border-radius-sm: 6px;
|
||||||
|
--border-radius-base: 8px;
|
||||||
|
--border-radius-md: 12px;
|
||||||
|
--border-radius-lg: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: var(--bg-base);
|
||||||
|
--text-color: var(--text-primary);
|
||||||
|
--text-muted: var(--text-secondary);
|
||||||
|
--card-bg: var(--surface-base);
|
||||||
|
--border-color: var(--border-base);
|
||||||
|
|
||||||
|
--lora-accent: var(--color-accent);
|
||||||
|
--lora-surface: var(--bg-elevated);
|
||||||
|
--lora-border: var(--border-subtle);
|
||||||
|
--lora-text: var(--text-primary);
|
||||||
|
--lora-error: var(--color-error);
|
||||||
|
--lora-error-bg: var(--color-error-bg);
|
||||||
|
--lora-error-border: var(--color-error-border);
|
||||||
|
--lora-warning: var(--color-warning);
|
||||||
|
--lora-success: var(--color-success);
|
||||||
|
|
||||||
|
--badge-update-bg: var(--color-info-bg);
|
||||||
|
--badge-update-text: var(--color-info-text);
|
||||||
|
--badge-update-glow: var(--color-info-glow);
|
||||||
|
--badge-skip-refresh-bg: var(--color-skip-refresh-bg);
|
||||||
|
--badge-skip-refresh-text: var(--color-skip-refresh-text);
|
||||||
|
--badge-skip-refresh-glow: var(--color-skip-refresh-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-color: var(--bg-base);
|
||||||
|
--text-color: var(--text-primary);
|
||||||
|
--text-muted: var(--text-secondary);
|
||||||
|
--card-bg: var(--surface-base);
|
||||||
|
--border-color: var(--border-base);
|
||||||
|
|
||||||
|
--lora-accent: var(--color-accent);
|
||||||
|
--lora-surface: var(--bg-elevated);
|
||||||
|
--lora-border: var(--border-subtle);
|
||||||
|
--lora-text: var(--text-primary);
|
||||||
|
--lora-error: var(--color-error);
|
||||||
|
--lora-error-bg: var(--color-error-bg);
|
||||||
|
--lora-error-border: var(--color-error-border);
|
||||||
|
--lora-warning: var(--color-warning);
|
||||||
|
--lora-success: var(--color-success);
|
||||||
|
|
||||||
|
--badge-update-bg: var(--color-info-bg);
|
||||||
|
--badge-update-text: var(--color-info-text);
|
||||||
|
--badge-update-glow: var(--color-info-glow);
|
||||||
|
--badge-skip-refresh-bg: var(--color-skip-refresh-bg);
|
||||||
|
--badge-skip-refresh-text: var(--color-skip-refresh-text);
|
||||||
|
--badge-skip-refresh-glow: var(--color-skip-refresh-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="dark"] {
|
html[data-theme="dark"] {
|
||||||
background-color: #1a1a1a !important;
|
background-color: var(--bg-base) !important;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="light"] {
|
html[data-theme="light"] {
|
||||||
background-color: #ffffff !important;
|
background-color: var(--bg-base) !important;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--bg-color: #1a1a1a;
|
|
||||||
--text-color: #e0e0e0;
|
|
||||||
--text-muted: #a0a0a0;
|
|
||||||
--card-bg: #2d2d2d;
|
|
||||||
--border-color: #404040;
|
|
||||||
|
|
||||||
--lora-accent: oklch(68% 0.28 256);
|
|
||||||
--lora-surface: oklch(25% 0.02 256 / 0.98);
|
|
||||||
--lora-border: oklch(90% 0.02 256 / 0.15);
|
|
||||||
--lora-text: oklch(98% 0.02 256);
|
|
||||||
--lora-warning: oklch(75% 0.25 80);
|
|
||||||
/* Modified to be used with oklch() */
|
|
||||||
--lora-error-bg: color-mix(in oklch, var(--lora-error) 15%, transparent);
|
|
||||||
--lora-error-border: color-mix(in oklch, var(--lora-error) 40%, transparent);
|
|
||||||
--badge-update-bg: oklch(62% 0.18 220);
|
|
||||||
--badge-update-text: oklch(98% 0.02 240);
|
|
||||||
--badge-update-glow: oklch(62% 0.18 220 / 0.4);
|
|
||||||
--badge-skip-refresh-bg: oklch(82% 0.12 45);
|
|
||||||
--badge-skip-refresh-text: oklch(98% 0.02 45);
|
|
||||||
--badge-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: var(--font-body);
|
||||||
background: var(--bg-color);
|
background: var(--bg-base);
|
||||||
color: var(--text-color);
|
color: var(--text-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
/* Remove the padding-top */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible),
|
||||||
|
input:focus:not(:focus-visible),
|
||||||
|
select:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-side);
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-side);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-alphabet-bar:hover {
|
.toggle-alphabet-bar:hover {
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
color: white;
|
color: white;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.letter-chip.active {
|
.letter-chip.active {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
color: white;
|
color: white;
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tertiary Action Button */
|
/* Tertiary Action Button */
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn:hover {
|
.back-btn:hover {
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* 卡片网格布局 */
|
/* Card grid layout */
|
||||||
.card-grid {
|
.card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); /* Base size */
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); /* Base size */
|
||||||
gap: 12px; /* Consistent gap for both row and column spacing */
|
gap: 12px; /* Consistent gap for both row and column spacing */
|
||||||
row-gap: 20px; /* Increase vertical spacing between rows */
|
row-gap: 20px; /* Increase vertical spacing between rows */
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
padding-top: 4px; /* 添加顶部内边距,为悬停动画提供空间 */
|
padding-top: 4px;
|
||||||
padding-bottom: 4px; /* 添加底部内边距,为悬停动画提供空间 */
|
padding-bottom: 4px;
|
||||||
width: 100%; /* Ensure it takes full width of container */
|
width: 100%; /* Ensure it takes full width of container */
|
||||||
max-width: 1400px; /* Base container width */
|
max-width: 1400px; /* Base container width */
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
transition: transform 160ms ease-out;
|
transition: transform var(--transition-fast) ease-out, box-shadow var(--transition-fast) ease-out, border-color var(--transition-fast) ease-out;
|
||||||
aspect-ratio: 896/1152; /* Preserve aspect ratio */
|
aspect-ratio: 896/1152; /* Preserve aspect ratio */
|
||||||
max-width: 260px; /* Base size */
|
max-width: 260px; /* Base size */
|
||||||
min-width: 200px; /* Prevent cards from becoming too narrow */
|
min-width: 200px; /* Prevent cards from becoming too narrow */
|
||||||
@@ -33,7 +33,8 @@
|
|||||||
|
|
||||||
.model-card:hover {
|
.model-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
background: oklch(100% 0 0 / 0.6);
|
box-shadow: var(--shadow-md);
|
||||||
|
border-color: var(--lora-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card:focus-visible {
|
.model-card:focus-visible {
|
||||||
@@ -353,21 +354,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
.card-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-1); /* Use gap instead of margin for spacing between icons */
|
gap: var(--space-1);
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions i:hover {
|
.card-actions i:hover,
|
||||||
|
.card-actions i:focus-visible {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
outline: 2px solid var(--lora-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for active favorites */
|
|
||||||
.favorite-active {
|
.favorite-active {
|
||||||
color: #ffc107 !important; /* Gold color for favorites */
|
color: var(--favorite-color) !important;
|
||||||
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
text-shadow: 0 0 5px var(--favorite-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
@@ -391,14 +397,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-actions {
|
|
||||||
flex-shrink: 0; /* Prevent actions from shrinking */
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-1);
|
|
||||||
align-items: flex-end; /* 将图标靠下对齐 */
|
|
||||||
align-self: flex-end; /* 将整个actions容器靠下对齐 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.model-link {
|
.model-link {
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
}
|
}
|
||||||
@@ -411,9 +409,13 @@
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-link a:hover {
|
.model-link a:hover,
|
||||||
|
.model-link a:focus-visible {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
outline: 2px solid var(--lora-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Updated model name to fix text cutoff issues */
|
/* Updated model name to fix text cutoff issues */
|
||||||
@@ -438,7 +440,7 @@
|
|||||||
|
|
||||||
.base-model {
|
.base-model {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: #f0f0f0;
|
background: var(--surface-hover, oklch(95% 0 0));
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
border-bottom: 1px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.4); /* Make bottom border stronger */
|
border-bottom: 1px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.4); /* Make bottom border stronger */
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); /* Stronger shadow */
|
box-shadow: var(--shadow-lg); /* Stronger shadow */
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicates-banner button.btn-exit-mode:hover {
|
.duplicates-banner button.btn-exit-mode:hover {
|
||||||
@@ -86,16 +86,16 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicates-banner button:hover {
|
.duplicates-banner button:hover {
|
||||||
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
|
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.duplicates-banner button.btn-exit {
|
.duplicates-banner button.btn-exit {
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); /* Add subtle shadow to groups */
|
box-shadow: var(--shadow-md); /* Add subtle shadow to groups */
|
||||||
/* Add responsive width settings to match banner */
|
/* Add responsive width settings to match banner */
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -173,9 +173,9 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
|
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-group-container {
|
.card-group-container {
|
||||||
@@ -230,20 +230,20 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-sm);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-toggle-btn:hover {
|
.group-toggle-btn:hover {
|
||||||
border-color: var(--lora-accent-l) var(--lora-accent-c) var (--lora-accent-h);
|
border-color: var(--lora-accent-l) var(--lora-accent-c) var (--lora-accent-h);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Duplicate card styling */
|
/* Duplicate card styling */
|
||||||
.model-card.duplicate {
|
.model-card.duplicate {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card.duplicate:hover {
|
.model-card.duplicate:hover {
|
||||||
@@ -257,7 +257,7 @@
|
|||||||
|
|
||||||
.model-card.duplicate-selected {
|
.model-card.duplicate-selected {
|
||||||
border: 2px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
|
border: 2px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card .selector-checkbox {
|
.model-card .selector-checkbox {
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
box-shadow: var(--shadow-lg);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-verify-hashes:hover {
|
.btn-verify-hashes:hover {
|
||||||
@@ -461,7 +461,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: -8px; /* Moved closer to button */
|
top: -8px; /* Moved closer to button */
|
||||||
right: -8px; /* Moved closer to button */
|
right: -8px; /* Moved closer to button */
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); /* Softer shadow */
|
box-shadow: var(--shadow-sm); /* Softer shadow */
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@
|
|||||||
cursor: help;
|
cursor: help;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-icon:hover {
|
.help-icon:hover {
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-elevated);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
@@ -572,16 +572,16 @@
|
|||||||
|
|
||||||
/* In dark mode, add additional distinction */
|
/* In dark mode, add additional distinction */
|
||||||
html[data-theme="dark"] .duplicates-banner {
|
html[data-theme="dark"] .duplicates-banner {
|
||||||
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4); /* Stronger shadow in dark mode */
|
box-shadow: var(--shadow-dark-lg); /* Stronger shadow in dark mode */
|
||||||
background-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15); /* Slightly stronger background in dark mode */
|
background-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15); /* Slightly stronger background in dark mode */
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="dark"] .duplicate-group {
|
html[data-theme="dark"] .duplicate-group {
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); /* Stronger shadow in dark mode */
|
box-shadow: var(--shadow-lg); /* Stronger shadow in dark mode */
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="dark"] .help-tooltip {
|
html[data-theme="dark"] .help-tooltip {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-elevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for disabled controls during duplicates mode */
|
/* Styles for disabled controls during duplicates mode */
|
||||||
|
|||||||
@@ -7,22 +7,22 @@
|
|||||||
color: white;
|
color: white;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
border: 1px solid var(--lora-accent);
|
border: 1px solid var(--lora-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-sm);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group .filter-active:hover {
|
.control-group .filter-active:hover {
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group .filter-active:active {
|
.control-group .filter-active:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group .filter-active i.fa-filter {
|
.control-group .filter-active i.fa-filter {
|
||||||
@@ -59,9 +59,9 @@
|
|||||||
|
|
||||||
/* Animation for filter indicator */
|
/* Animation for filter indicator */
|
||||||
@keyframes filterPulse {
|
@keyframes filterPulse {
|
||||||
0% { transform: scale(1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
|
0% { transform: scale(1); box-shadow: var(--shadow-sm); }
|
||||||
50% { transform: scale(1.03); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); }
|
50% { transform: scale(1.03); box-shadow: var(--shadow-lg); }
|
||||||
100% { transform: scale(1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
|
100% { transform: scale(1); box-shadow: var(--shadow-sm); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-active.animate {
|
.filter-active.animate {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
/* Reduced height */
|
/* Reduced height */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
/* Slightly stronger shadow */
|
/* Slightly stronger shadow */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,14 +134,14 @@
|
|||||||
background: var(--input-bg, var(--card-bg));
|
background: var(--input-bg, var(--card-bg));
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm, 6px);
|
border-radius: var(--border-radius-sm, 6px);
|
||||||
transition: all 0.2s ease;
|
transition: border-color var(--transition-base), box-shadow var(--transition-base);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-header);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search .search-container:focus-within {
|
.header-search .search-container:focus-within {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 0 1px var(--lora-accent);
|
box-shadow: var(--shadow-header), 0 0 0 1px var(--lora-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search input {
|
.header-search input {
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--border-radius-xs, 4px);
|
border-radius: var(--border-radius-xs, 4px);
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search .search-options-toggle {
|
.header-search .search-options-toggle {
|
||||||
@@ -191,9 +191,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-search .search-options-toggle:hover,
|
.header-search .search-options-toggle:hover,
|
||||||
.header-search .search-filter-toggle:hover {
|
.header-search .search-filter-toggle:hover,
|
||||||
|
.header-search .search-filter-toggle:focus-visible {
|
||||||
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
color: var(--lora-accent);
|
color: var(--lora-accent);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search .filter-badge {
|
.header-search .filter-badge {
|
||||||
@@ -269,7 +271,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base), transform var(--transition-base);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +343,7 @@
|
|||||||
background-color: var(--lora-error);
|
background-color: var(--lora-error);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid var(--card-bg);
|
border: 2px solid var(--card-bg);
|
||||||
transition: all 0.2s ease;
|
transition: opacity var(--transition-base);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -362,13 +364,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger-menu-btn:hover {
|
.hamburger-menu-btn:hover,
|
||||||
background: var(--lora-accent);
|
.hamburger-menu-btn:focus-visible {
|
||||||
color: white;
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
|
color: var(--lora-accent);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown .dropdown-item:hover,
|
||||||
|
.hamburger-dropdown .dropdown-item:focus-visible {
|
||||||
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
|
color: var(--lora-accent);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hamburger dropdown menu */
|
/* Hamburger dropdown menu */
|
||||||
@@ -381,7 +392,7 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm, 6px);
|
border-radius: var(--border-radius-sm, 6px);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-toast);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
min-width: 160px;
|
min-width: 160px;
|
||||||
z-index: var(--z-dropdown, 200);
|
z-index: var(--z-dropdown, 200);
|
||||||
@@ -401,7 +412,7 @@
|
|||||||
border-radius: var(--border-radius-xs, 4px);
|
border-radius: var(--border-radius-xs, 4px);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -757,7 +757,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
background: rgba(var(--lora-accent), 0.05);
|
background: rgba(var(--lora-accent), 0.05);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tips-header {
|
.tips-header {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: help;
|
cursor: help;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
color: white;
|
color: white;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.keyboard-nav-hint i {
|
.keyboard-nav-hint i {
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
transform: translateY(-15%); /* Vertically center */
|
transform: translateY(-15%); /* Vertically center */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-lg);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
@@ -92,5 +92,5 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
width: min(400px, 90vw); /* 固定最大宽度,但保持响应式 */
|
width: min(400px, 90vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
.loading-status {
|
.loading-status {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: var(--text-color); /* 使用主题文本颜色 */
|
color: var(--text-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -42,11 +42,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-container {
|
.progress-container {
|
||||||
width: 280px; /* 固定进度条宽度 */
|
width: 280px;
|
||||||
background-color: var(--lora-border); /* 使用主题边框颜色 */
|
background-color: var(--lora-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto; /* 居中显示 */
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.model-description-content code {
|
.model-description-content code {
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
|
|||||||
@@ -105,14 +105,14 @@
|
|||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整深色主题下的样式 */
|
/* Dark theme info item styles */
|
||||||
[data-theme="dark"] .info-item {
|
[data-theme="dark"] .info-item {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,18 +140,70 @@
|
|||||||
|
|
||||||
/* Add specific styles for notes content */
|
/* Add specific styles for notes content */
|
||||||
.info-item.notes .editable-field [contenteditable] {
|
.info-item.notes .editable-field [contenteditable] {
|
||||||
height: 60px; /* Keep initial modal layout stable regardless of note length */
|
min-height: 60px;
|
||||||
min-height: 60px; /* Increase height for multiple lines */
|
white-space: pre-wrap;
|
||||||
max-height: 420px; /* Limit maximum height */
|
line-height: 1.5;
|
||||||
overflow: auto; /* Enable scrolling and resize handle for long content */
|
padding: 8px 12px;
|
||||||
resize: vertical; /* Allow manual vertical resizing */
|
}
|
||||||
white-space: pre-wrap; /* Preserve line breaks */
|
|
||||||
line-height: 1.5; /* Improve readability */
|
/* Notes expand/collapse — collapsed by default; only applies when JS detects long content */
|
||||||
padding: 8px 12px; /* Slightly increase padding */
|
.info-item.notes .editable-field {
|
||||||
|
position: relative;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item.notes .editable-field.collapsed {
|
||||||
|
max-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient fade overlay hint when collapsed */
|
||||||
|
.info-item.notes .editable-field.collapsed::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 28px;
|
||||||
|
background: linear-gradient(transparent, var(--bg-color));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notes header row — label left, toggle button right */
|
||||||
|
.notes-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle button — icon only, inline with the label */
|
||||||
|
.notes-toggle-btn {
|
||||||
|
display: none; /* shown by JS when content exceeds threshold */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-toggle-btn:hover {
|
||||||
|
background: rgba(66, 153, 225, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-toggle-btn i {
|
||||||
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-path {
|
.file-path {
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +271,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修改 back-to-top 按钮样式,使其固定在 modal 内部 */
|
/* Back-to-top button pinned inside modal */
|
||||||
.modal-content .back-to-top {
|
.modal-content .back-to-top {
|
||||||
position: sticky; /* 改用 sticky 定位 */
|
position: sticky;
|
||||||
float: right; /* 使用 float 确保按钮在右侧 */
|
float: right;
|
||||||
bottom: 20px; /* 距离底部的距离 */
|
bottom: 20px;
|
||||||
margin-right: 20px; /* 右侧间距 */
|
margin-right: 20px;
|
||||||
margin-top: -56px; /* 负边距确保不占用额外空间 */
|
margin-top: -56px;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -239,7 +291,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
transition: all 0.3s ease;
|
transition: opacity var(--transition-slow), visibility var(--transition-slow), transform var(--transition-slow);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +334,7 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 合并编辑按钮样式 */
|
/* Consolidated edit button styles */
|
||||||
.edit-model-name-btn,
|
.edit-model-name-btn,
|
||||||
.edit-file-name-btn,
|
.edit-file-name-btn,
|
||||||
.edit-base-model-btn,
|
.edit-base-model-btn,
|
||||||
@@ -295,7 +347,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: opacity var(--transition-base), background-color var(--transition-base);
|
||||||
margin-left: var(--space-1);
|
margin-left: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,7 +369,7 @@
|
|||||||
.edit-base-model-btn:hover,
|
.edit-base-model-btn:hover,
|
||||||
.edit-model-description-btn:hover,
|
.edit-model-description-btn:hover,
|
||||||
.edit-version-name-btn:hover {
|
.edit-version-name-btn:hover {
|
||||||
opacity: 0.8 !important;
|
opacity: 0.8;
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +387,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.base-wrapper {
|
.base-wrapper {
|
||||||
flex: 2; /* 分配更多空间给base model */
|
flex: 2; /* Allocate more space to base model */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base model display and editing styles */
|
/* Base model display and editing styles */
|
||||||
@@ -378,7 +430,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.size-wrapper span {
|
.size-wrapper span {
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
@@ -395,7 +447,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--space-1);
|
padding: var(--space-1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
font-size: 1.5em !important;
|
font-size: 1.5em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -431,7 +483,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -836,18 +888,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 2px 10px;
|
padding: 2px 10px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .creator-info,
|
[data-theme="dark"] .creator-info,
|
||||||
[data-theme="dark"] .civitai-view,
|
[data-theme="dark"] .civitai-view,
|
||||||
[data-theme="dark"] .modal-send-btn {
|
[data-theme="dark"] .modal-send-btn {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -906,14 +958,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.civitai-view i {
|
.civitai-view i {
|
||||||
@@ -929,18 +981,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .modal-send-btn {
|
[data-theme="dark"] .modal-send-btn {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: calc(var(--space-1) * 0.5) var(--space-1);
|
padding: calc(var(--space-1) * 0.5) var(--space-1);
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-tag span {
|
.preset-tag span {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-tag:hover {
|
.preset-tag:hover {
|
||||||
|
|||||||
@@ -111,8 +111,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-md);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
|
|
||||||
.media-control-btn:hover {
|
.media-control-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-control-btn.set-preview-btn:hover {
|
.media-control-btn.set-preview-btn:hover {
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
z-index: 5;
|
z-index: 5;
|
||||||
max-height: 50%; /* Reduced to take less space */
|
max-height: 50%; /* Reduced to take less space */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-inset-top);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
/* Adjust to dark theme */
|
/* Adjust to dark theme */
|
||||||
[data-theme="dark"] .image-metadata-panel {
|
[data-theme="dark"] .image-metadata-panel {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-inset-top);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-content {
|
.metadata-content {
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
|
|
||||||
.metadata-prompt {
|
.metadata-prompt {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
@@ -312,7 +312,7 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-prompt-btn:hover {
|
.copy-prompt-btn:hover {
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -455,9 +455,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.import-formats {
|
.import-formats {
|
||||||
font-size: 0.8em !important;
|
font-size: 0.8em;
|
||||||
opacity: 0.6 !important;
|
opacity: 0.6;
|
||||||
margin-top: var(--space-2) !important;
|
margin-top: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-files-btn {
|
.select-files-btn {
|
||||||
@@ -471,7 +471,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-files-btn:hover {
|
.select-files-btn:hover {
|
||||||
@@ -481,7 +481,7 @@
|
|||||||
|
|
||||||
/* For dark theme */
|
/* For dark theme */
|
||||||
[data-theme="dark"] .import-container {
|
[data-theme="dark"] .import-container {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Setup Guidance State - When example images path is not configured */
|
/* Setup Guidance State - When example images path is not configured */
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
.model-tag-compact {
|
.model-tag-compact {
|
||||||
/* Updated styles to match info-item appearance */
|
/* Updated styles to match info-item appearance */
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
/* Adjust dark theme tag styles */
|
/* Adjust dark theme tag styles */
|
||||||
[data-theme="dark"] .model-tag-compact {
|
[data-theme="dark"] .model-tag-compact {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,14 +73,14 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-lg);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
|
|
||||||
.tooltip-tag {
|
.tooltip-tag {
|
||||||
/* Updated styles to match info-item appearance */
|
/* Updated styles to match info-item appearance */
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
/* Adjust dark theme tooltip tag styles */
|
/* Adjust dark theme tooltip tag styles */
|
||||||
[data-theme="dark"] .tooltip-tag {
|
[data-theme="dark"] .tooltip-tag {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-left: var(--space-1);
|
margin-left: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* Update Trigger Words styles */
|
/* Update Trigger Words styles */
|
||||||
.info-item.trigger-words {
|
.info-item.trigger-words {
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整 trigger words 样式 */
|
/* Trigger words styles */
|
||||||
[data-theme="dark"] .info-item.trigger-words {
|
[data-theme="dark"] .info-item.trigger-words {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
background: color-mix(in oklch, var(--card-bg) 92%, var(--bg-color) 8%);
|
background: color-mix(in oklch, var(--card-bg) 92%, var(--bg-color) 8%);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
box-shadow: var(--shadow-xs);
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
|
|
||||||
.model-version-row:hover {
|
.model-version-row:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-version-row.is-clickable {
|
.model-version-row.is-clickable {
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
height: 88px;
|
height: 88px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-hover);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
box-shadow: var(--shadow-dark-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-viewer-video {
|
.media-viewer-video {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-dropdown);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: none;
|
display: none;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -21,9 +21,11 @@
|
|||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-item:hover {
|
.context-menu-item:hover,
|
||||||
|
.context-menu-item:focus-visible {
|
||||||
background-color: var(--lora-accent);
|
background-color: var(--lora-accent);
|
||||||
color: var(--lora-text);
|
color: var(--lora-text);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-separator {
|
.context-menu-separator {
|
||||||
@@ -32,6 +34,12 @@
|
|||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lighter separator between category groups (vs the full separator before destructive) */
|
||||||
|
.context-menu-separator.menu-section-break {
|
||||||
|
opacity: 0.4;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu-item.delete-item {
|
.context-menu-item.delete-item {
|
||||||
color: var(--danger-color);
|
color: var(--danger-color);
|
||||||
}
|
}
|
||||||
@@ -75,7 +83,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-dropdown);
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
@@ -108,7 +116,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-modal);
|
||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
width: 300px;
|
width: 300px;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -162,7 +170,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw-level-btn:hover {
|
.nsfw-level-btn:hover {
|
||||||
@@ -186,7 +194,7 @@
|
|||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-dropdown);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
display: none;
|
display: none;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* modal 基础样式 */
|
/* Modal base styles */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -6,19 +6,19 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - var(--header-height, 48px)); /* Adjust height to exclude header */
|
height: calc(100% - var(--header-height, 48px)); /* Adjust height to exclude header */
|
||||||
background: rgba(0, 0, 0, 0.2); /* 调整为更淡的半透明黑色 */
|
background: rgba(0, 0, 0, 0.2);
|
||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
overflow: auto; /* Change from hidden to auto to allow scrolling */
|
overflow: auto; /* Change from hidden to auto to allow scrolling */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当模态窗口打开时,禁止body滚动 */
|
/* Prevent body scroll when modal is open */
|
||||||
body.modal-open {
|
body.modal-open {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-right: var(--scrollbar-width, 0px); /* 补偿滚动条消失导致的页面偏移 */
|
padding-right: var(--scrollbar-width, 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* modal-content 样式 */
|
/* Modal content styles */
|
||||||
.modal-content {
|
.modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
@@ -29,12 +29,9 @@ body.modal-open {
|
|||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
box-shadow:
|
box-shadow: var(--shadow-md);
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
|
||||||
0 10px 15px -3px rgba(0, 0, 0, 0.05);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden; /* 防止水平滚动条 */
|
overflow-x: hidden;
|
||||||
scrollbar-gutter: stable both-edges; /* Reserve space to prevent layout shift when scrollbar toggles */
|
scrollbar-gutter: stable both-edges; /* Reserve space to prevent layout shift when scrollbar toggles */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +39,10 @@ body.modal-open {
|
|||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 当 modal 打开时锁定 body */
|
/* Lock body when modal is open */
|
||||||
body.modal-open {
|
body.modal-open {
|
||||||
overflow: hidden !important; /* 覆盖 base.css 中的 scroll */
|
overflow: hidden !important;
|
||||||
padding-right: var(--scrollbar-width, 8px); /* 使用滚动条宽度作为补偿 */
|
padding-right: var(--scrollbar-width, 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalFadeIn {
|
@keyframes modalFadeIn {
|
||||||
@@ -67,12 +64,25 @@ body.modal-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn, .delete-btn, .exclude-btn, .confirm-btn {
|
.cancel-btn, .delete-btn, .exclude-btn, .confirm-btn {
|
||||||
padding: 8px var(--space-2);
|
display: flex;
|
||||||
border-radius: 6px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: 0.95em;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
|
transition: background-color var(--transition-base), opacity var(--transition-base), transform var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:active,
|
||||||
|
.delete-btn:active,
|
||||||
|
.exclude-btn:active,
|
||||||
|
.confirm-btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
@@ -92,16 +102,20 @@ body.modal-open {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn:hover {
|
.cancel-btn:hover,
|
||||||
|
.cancel-btn:focus-visible {
|
||||||
background: var(--lora-border);
|
background: var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-btn:hover {
|
.delete-btn:hover,
|
||||||
opacity: 0.9;
|
.delete-btn:focus-visible {
|
||||||
|
background: oklch(from var(--lora-error) l c h / 85%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.exclude-btn:hover, .confirm-btn:hover {
|
.exclude-btn:hover,
|
||||||
opacity: 0.9;
|
.exclude-btn:focus-visible,
|
||||||
|
.confirm-btn:hover,
|
||||||
|
.confirm-btn:focus-visible {
|
||||||
background: oklch(from var(--lora-accent, #4f46e5) l c h / 85%);
|
background: oklch(from var(--lora-accent, #4f46e5) l c h / 85%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,47 +135,41 @@ body.modal-open {
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: opacity 0.2s;
|
transition: opacity var(--transition-base);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close:hover {
|
.close:hover,
|
||||||
|
.close:focus-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
outline: 2px solid var(--lora-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统一各个 section 的样式 */
|
/* Unified section styles */
|
||||||
.support-section,
|
.support-section,
|
||||||
.changelog-section,
|
.changelog-section,
|
||||||
.update-info,
|
.update-info,
|
||||||
.info-item,
|
.info-item,
|
||||||
.path-preview {
|
.path-preview {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色主题统一样式 */
|
/* Dark theme unified styles */
|
||||||
[data-theme="dark"] .modal-content {
|
[data-theme="dark"] .modal-content {
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .support-section,
|
|
||||||
[data-theme="dark"] .changelog-section,
|
|
||||||
[data-theme="dark"] .update-info,
|
|
||||||
[data-theme="dark"] .info-item,
|
|
||||||
[data-theme="dark"] .path-preview,
|
|
||||||
[data-theme="dark"] #bulkDownloadMissingLorasModal .bulk-download-loras-preview {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border: 1px solid var(--lora-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn {
|
.primary-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: var(--space-1) var(--space-2);
|
||||||
background-color: var(--lora-accent);
|
background-color: var(--lora-accent);
|
||||||
color: var(--lora-text);
|
color: var(--lora-text);
|
||||||
border: none;
|
border: none;
|
||||||
@@ -171,9 +179,11 @@ body.modal-open {
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn:hover {
|
.primary-btn:hover,
|
||||||
|
.primary-btn:focus-visible {
|
||||||
background-color: oklch(from var(--lora-accent) l c h / 85%);
|
background-color: oklch(from var(--lora-accent) l c h / 85%);
|
||||||
color: var(--lora-text);
|
color: var(--lora-text);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Secondary button styles */
|
/* Secondary button styles */
|
||||||
@@ -181,19 +191,21 @@ body.modal-open {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: var(--space-1) var(--space-2);
|
||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
color: var (--text-color);
|
color: var(--text-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-btn:hover {
|
.secondary-btn:hover,
|
||||||
|
.secondary-btn:focus-visible {
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled button styles */
|
/* Disabled button styles */
|
||||||
@@ -244,7 +256,7 @@ button:disabled,
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: var(--space-1) var(--space-2);
|
||||||
background-color: var(--lora-error);
|
background-color: var(--lora-error);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -254,25 +266,22 @@ button:disabled,
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-btn:hover {
|
.danger-btn:hover,
|
||||||
|
.danger-btn:focus-visible {
|
||||||
background-color: oklch(from var(--lora-error) l c h / 85%);
|
background-color: oklch(from var(--lora-error) l c h / 85%);
|
||||||
color: white;
|
color: white;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Metadata archive status styles */
|
/* Metadata archive status styles */
|
||||||
.metadata-archive-status {
|
.metadata-archive-status {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .metadata-archive-status {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border: 1px solid var(--lora-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.archive-status-item {
|
.archive-status-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -312,17 +321,12 @@ button:disabled,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.backup-status {
|
.backup-status {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .backup-status {
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border: 1px solid var(--lora-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-summary-grid {
|
.backup-summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
@@ -331,17 +335,12 @@ button:disabled,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.backup-summary-card {
|
.backup-summary-card {
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: var(--lora-surface);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .backup-summary-card {
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border-color: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.backup-summary-label {
|
.backup-summary-label {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -404,14 +403,9 @@ button:disabled,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.backup-location-details {
|
.backup-location-details {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: var(--surface-subtle);
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .backup-location-details {
|
|
||||||
border-color: var(--lora-border);
|
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.backup-location-details summary {
|
.backup-location-details summary {
|
||||||
@@ -442,16 +436,12 @@ button:disabled,
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: var(--surface-subtle);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .backup-location-path {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.backup-status-row {
|
.backup-status-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -519,8 +509,8 @@ button:disabled,
|
|||||||
}
|
}
|
||||||
|
|
||||||
#bulkDownloadMissingLorasModal .bulk-download-loras-preview {
|
#bulkDownloadMissingLorasModal .bulk-download-loras-preview {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
margin-bottom: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
@@ -578,7 +568,7 @@ button:disabled,
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: rgba(59, 130, 246, 0.1);
|
background: oklch(from var(--lora-accent) l c h / 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,7 @@
|
|||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.doctor-kicker {
|
.doctor-kicker {
|
||||||
@@ -128,7 +127,7 @@
|
|||||||
|
|
||||||
.doctor-issue-card {
|
.doctor-issue-card {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -242,7 +241,7 @@
|
|||||||
|
|
||||||
[data-theme="dark"] .doctor-hero,
|
[data-theme="dark"] .doctor-hero,
|
||||||
[data-theme="dark"] .doctor-issue-card {
|
[data-theme="dark"] .doctor-issue-card {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border-color: var(--lora-border);
|
border-color: var(--lora-border);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
.version-item:hover {
|
.version-item:hover {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -272,7 +272,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-expand-icon:hover {
|
.tree-expand-icon:hover {
|
||||||
@@ -364,7 +364,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-folder-form button.confirm {
|
.create-folder-form button.confirm {
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
.path-display {
|
.path-display {
|
||||||
padding: var(--space-1);
|
padding: var(--space-1);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -453,7 +453,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: var(--border-color);
|
background-color: var(--border-color);
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,9 +465,9 @@
|
|||||||
left: 3px;
|
left: 3px;
|
||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-toggle-container .toggle-switch input:checked+.toggle-slider {
|
.inline-toggle-container .toggle-switch input:checked+.toggle-slider {
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
border: 1px solid oklch(var(--lora-accent) / 0.35);
|
border: 1px solid oklch(var(--lora-accent) / 0.35);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-shadow: 0 1px 2px oklch(var(--lora-accent) / 0.1);
|
box-shadow: 0 1px 2px oklch(var(--lora-accent) / 0.1);
|
||||||
@@ -577,13 +577,13 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-option:hover {
|
.file-option:hover {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-option.selected {
|
.file-option.selected {
|
||||||
@@ -669,3 +669,156 @@
|
|||||||
background: oklch(0.5 0.08 160 / 0.15);
|
background: oklch(0.5 0.08 160 / 0.15);
|
||||||
color: oklch(0.65 0.08 160);
|
color: oklch(0.65 0.08 160);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Textarea for multi-URL input */
|
||||||
|
#modelUrl {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.9em;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.85em;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hint i {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Batch Preview List */
|
||||||
|
.batch-preview-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
background: var(--border-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-item:first-child {
|
||||||
|
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-item:last-child {
|
||||||
|
border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-item:only-child {
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-thumbnail {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--lora-error);
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-error-text {
|
||||||
|
color: var(--lora-error);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-local-badge {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-local {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-change-version {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-remove {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--lora-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-error {
|
||||||
|
background: oklch(0.5 0.15 25 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .batch-preview-item {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
background-color: var(--lora-surface);
|
background-color: var(--lora-surface);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-option-btn:hover {
|
.example-option-btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,5 +68,5 @@
|
|||||||
|
|
||||||
/* Dark theme adjustments */
|
/* Dark theme adjustments */
|
||||||
[data-theme="dark"] .example-option-btn:hover {
|
[data-theme="dark"] .example-option-btn:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
box-shadow: var(--shadow-elevated);
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-sm);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
|
|
||||||
/* Dark theme adjustments for new content badge */
|
/* Dark theme adjustments for new content badge */
|
||||||
[data-theme="dark"] .new-content-badge {
|
[data-theme="dark"] .new-content-badge {
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Update video list styles */
|
/* Update video list styles */
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
animation: fadeIn 0.5s ease-in-out;
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-date-badge i {
|
.update-date-badge i {
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
|
|
||||||
/* Dark theme adjustments */
|
/* Dark theme adjustments */
|
||||||
[data-theme="dark"] .update-date-badge {
|
[data-theme="dark"] .update-date-badge {
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Privacy-friendly video embed styles */
|
/* Privacy-friendly video embed styles */
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
background-color: var(--lora-accent);
|
background-color: var(--lora-accent);
|
||||||
color: white;
|
color: white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -303,5 +303,5 @@
|
|||||||
|
|
||||||
/* Dark theme adjustments */
|
/* Dark theme adjustments */
|
||||||
[data-theme="dark"] .video-container {
|
[data-theme="dark"] .video-container {
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
background-color: var(--surface-hover);
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-toggle:hover {
|
.settings-toggle:hover {
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
background-color: var(--lora-surface);
|
background-color: var(--lora-surface);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-search-input:focus {
|
.settings-search-input:focus {
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-search-clear:hover {
|
.settings-search-clear:hover {
|
||||||
@@ -289,7 +289,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -582,7 +582,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.priority-tags-example code {
|
.priority-tags-example code {
|
||||||
font-family: var(--code-font, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace);
|
font-family: var(--font-mono);
|
||||||
background-color: rgba(var(--lora-accent-rgb, 79, 70, 229), 0.12);
|
background-color: rgba(var(--lora-accent-rgb, 79, 70, 229), 0.12);
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
@@ -614,7 +614,7 @@
|
|||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,19 +927,19 @@ input:checked + .toggle-slider:before {
|
|||||||
|
|
||||||
/* Path Template Settings Styles */
|
/* Path Template Settings Styles */
|
||||||
.template-preview {
|
.template-preview {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: var(--space-1);
|
padding: var(--space-1);
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--lora-accent);
|
color: var(--lora-accent);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .template-preview {
|
[data-theme="dark"] .template-preview {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,7 +974,7 @@ input:checked + .toggle-slider:before {
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
height: 32px; /* Match other control heights */
|
height: 32px; /* Match other control heights */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,7 +1030,7 @@ input:checked + .toggle-slider:before {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-mapping-btn:hover {
|
.remove-mapping-btn:hover {
|
||||||
@@ -1146,7 +1146,7 @@ input:checked + .toggle-slider:before {
|
|||||||
color: white;
|
color: white;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -1175,7 +1175,7 @@ input:checked + .toggle-slider:before {
|
|||||||
background-color: var(--lora-surface);
|
background-color: var(--lora-surface);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
height: 24px;
|
height: 24px;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
@@ -1277,7 +1277,7 @@ input:checked + .toggle-slider:before {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
font-family: var(--font-body);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@@ -1287,7 +1287,7 @@ input:checked + .toggle-slider:before {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-elevated);
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1309,7 +1309,7 @@ input:checked + .toggle-slider:before {
|
|||||||
/* Dark theme adjustments for tooltip - Fully opaque */
|
/* Dark theme adjustments for tooltip - Fully opaque */
|
||||||
[data-theme="dark"] .info-icon[data-tooltip]::after {
|
[data-theme="dark"] .info-icon[data-tooltip]::after {
|
||||||
background: rgba(40, 40, 40, 0.95);
|
background: rgba(40, 40, 40, 0.95);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
box-shadow: var(--shadow-dark-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra Folder Paths - Single input layout */
|
/* Extra Folder Paths - Single input layout */
|
||||||
@@ -1361,7 +1361,7 @@ input:checked + .toggle-slider:before {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-section {
|
.support-section {
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
@@ -102,7 +100,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-link:hover {
|
.social-link:hover {
|
||||||
@@ -122,14 +120,14 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.kofi-button:hover {
|
.kofi-button:hover {
|
||||||
background: #E04946;
|
background: #E04946;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Patreon button style */
|
/* Patreon button style */
|
||||||
@@ -144,14 +142,14 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.patreon-button:hover {
|
.patreon-button:hover {
|
||||||
background: #E04946;
|
background: #E04946;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* QR Code section styles */
|
/* QR Code section styles */
|
||||||
@@ -191,7 +189,7 @@
|
|||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
aspect-ratio: 1/1; /* Ensure proper aspect ratio for the square QR code */
|
aspect-ratio: 1/1; /* Ensure proper aspect ratio for the square QR code */
|
||||||
}
|
}
|
||||||
@@ -214,7 +212,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-toggle:hover {
|
.support-toggle:hover {
|
||||||
@@ -258,12 +256,12 @@
|
|||||||
color: white; /* Icon color changes to white on hover */
|
color: white; /* Icon color changes to white on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强hover状态的视觉反馈 */
|
/* Enhanced hover visual feedback */
|
||||||
.social-link:hover,
|
.social-link:hover,
|
||||||
.update-link:hover,
|
.update-link:hover,
|
||||||
.folder-item:hover {
|
.folder-item:hover {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Supporters Section Styles */
|
/* Supporters Section Styles */
|
||||||
@@ -349,14 +347,14 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-left: 3px solid var(--lora-accent);
|
border-left: 3px solid var(--lora-accent);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.supporter-special-card:hover {
|
.supporter-special-card:hover {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
border-left-color: var(--lora-accent);
|
border-left-color: var(--lora-accent);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-header);
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +439,7 @@
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.version-number {
|
.version-number {
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-link:hover {
|
.update-link:hover {
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
|
|
||||||
/* Update progress styles */
|
/* Update progress styles */
|
||||||
.update-progress {
|
.update-progress {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .update-progress {
|
[data-theme="dark"] .update-progress {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-info {
|
.progress-info {
|
||||||
@@ -234,8 +234,6 @@
|
|||||||
|
|
||||||
/* Changelog section */
|
/* Changelog section */
|
||||||
.changelog-section {
|
.changelog-section {
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
}
|
}
|
||||||
@@ -334,7 +332,7 @@
|
|||||||
background: rgba(0, 0, 0, 0.05);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +427,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .banner-history-item {
|
[data-theme="dark"] .banner-history-item {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner-history-title {
|
.banner-history-title {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-toast);
|
||||||
z-index: calc(var(--z-modal) - 1);
|
z-index: calc(var(--z-modal) - 1);
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -63,13 +63,21 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:hover {
|
.icon-button:hover,
|
||||||
opacity: 1;
|
.icon-button:focus-visible {
|
||||||
background: rgba(0, 0, 0, 0.05);
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
|
color: var(--lora-accent);
|
||||||
|
transform: scale(1.05);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .icon-button:hover,
|
||||||
|
[data-theme="dark"] .icon-button:focus-visible {
|
||||||
|
background: oklch(35% 0.02 256 / 0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .icon-button:hover {
|
[data-theme="dark"] .icon-button:hover {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 删除不再需要的按钮样式 */
|
/* Remove obsolete button styles */
|
||||||
.editor-actions {
|
.editor-actions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.recipe-tag-compact {
|
.recipe-tag-compact {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .recipe-tag-compact {
|
[data-theme="dark"] .recipe-tag-compact {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,14 +176,14 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-dropdown);
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-tag {
|
.tooltip-tag {
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tooltip-tag {
|
[data-theme="dark"] .tooltip-tag {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,19 +251,19 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .recipe-source-url-btn {
|
[data-theme="dark"] .recipe-source-url-btn {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-subtle);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-url-cancel-btn {
|
.source-url-cancel-btn {
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn:hover,
|
.copy-btn:hover,
|
||||||
@@ -705,7 +705,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -725,7 +725,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -797,7 +797,7 @@
|
|||||||
|
|
||||||
.recipe-lora-item:hover {
|
.recipe-lora-item:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-header);
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-header);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
@@ -1049,7 +1049,7 @@
|
|||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-family: monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,7 +1086,7 @@
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
transition: all 0.2s;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reconnect-cancel-btn {
|
.reconnect-cancel-btn {
|
||||||
@@ -1114,9 +1114,9 @@
|
|||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题输入框特定的样式 */
|
/* Title input specific styles */
|
||||||
.title-input {
|
.title-input {
|
||||||
font-size: 1.2em !important; /* 调整为更合适的大小 */
|
font-size: 1.2em;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
@@ -1251,7 +1251,7 @@
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-header);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 调整搜索框样式以匹配其他控件 */
|
/* Match search input styles to other controls */
|
||||||
.search-container input {
|
.search-container input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 35px 6px 12px; /* Reduced right padding */
|
padding: 6px 35px 6px 12px; /* Reduced right padding */
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 修改清空按钮样式 */
|
/* Clear button styles */
|
||||||
.search-clear {
|
.search-clear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 105px; /* Adjusted further left to avoid overlapping */
|
right: 105px; /* Adjusted further left to avoid overlapping */
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base), border-color var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: background-color var(--transition-base), color var(--transition-base), border-color var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
user-select: none; /* Prevent text selection */
|
user-select: none; /* Prevent text selection */
|
||||||
-webkit-user-select: none; /* For Safari */
|
-webkit-user-select: none; /* For Safari */
|
||||||
-moz-user-select: none; /* For Firefox */
|
-moz-user-select: none; /* For Firefox */
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +402,7 @@
|
|||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
@@ -470,7 +470,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 13px; /* Slightly smaller font size */
|
font-size: 13px; /* Slightly smaller font size */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -516,7 +516,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
background-color: var(--lora-surface);
|
background-color: var(--lora-surface);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,7 +574,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +599,7 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.25s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enabled state - visual cue that button is actionable */
|
/* Enabled state - visual cue that button is actionable */
|
||||||
@@ -726,7 +726,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-header);
|
||||||
z-index: 100; /* Higher z-index to ensure it's above other elements */
|
z-index: 100; /* Higher z-index to ensure it's above other elements */
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-header);
|
||||||
z-index: 100; /* Higher z-index to ensure it's above other elements */
|
z-index: 100; /* Higher z-index to ensure it's above other elements */
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-edit-btn:hover {
|
.metadata-edit-btn:hover {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
/* Edit Container */
|
/* Edit Container */
|
||||||
.metadata-edit-container {
|
.metadata-edit-container {
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .metadata-edit-container {
|
[data-theme="dark"] .metadata-edit-container {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--surface-hover);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.metadata-item-dragging {
|
.metadata-item-dragging {
|
||||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.25);
|
box-shadow: var(--shadow-dialog);
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
transition: none;
|
transition: none;
|
||||||
@@ -178,7 +178,7 @@ body.metadata-drag-active * {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metadata-edit-controls button:hover {
|
.metadata-edit-controls button:hover {
|
||||||
@@ -257,7 +257,7 @@ body.metadata-drag-active * {
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-elevated);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -299,7 +299,7 @@ body.metadata-drag-active * {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
border: 1px solid var(--lora-border);
|
border: 1px solid var(--lora-border);
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: var(--transition-slow);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-header);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
@@ -83,7 +83,8 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header:hover {
|
.sidebar-header:hover {
|
||||||
@@ -120,7 +121,7 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -150,6 +151,120 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar More Options Dropdown ===== */
|
||||||
|
.sidebar-more-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 8px;
|
||||||
|
min-width: 190px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: calc(var(--z-overlay) + 20);
|
||||||
|
display: none;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-more-dropdown.open {
|
||||||
|
display: block;
|
||||||
|
animation: dropdownFadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: var(--transition-base);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item:hover {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item:hover i {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dropdown-item.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar Hidden Indicator (left edge) ===== */
|
||||||
|
.sidebar-hidden-indicator {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: var(--z-overlay);
|
||||||
|
width: 14px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--border-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s ease, background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator i {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover i {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--text-color);
|
||||||
|
color: var(--bg-color);
|
||||||
|
font-size: 0.8em;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-hidden-indicator:hover .sidebar-hidden-indicator-tooltip {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-content {
|
.sidebar-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -174,7 +289,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -298,7 +413,7 @@
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -331,7 +446,7 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
@@ -364,7 +479,7 @@
|
|||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
|
box-shadow: var(--shadow-lg);
|
||||||
z-index: calc(var(--z-overlay) + 20);
|
z-index: calc(var(--z-overlay) + 20);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
@@ -382,7 +497,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb-dropdown-item:hover {
|
.breadcrumb-dropdown-item:hover {
|
||||||
@@ -406,7 +521,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -614,7 +729,7 @@
|
|||||||
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.08);
|
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.08);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
@@ -649,7 +764,7 @@
|
|||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-lg);
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
animation: slideUp 0.2s ease;
|
animation: slideUp 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -685,7 +800,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-folder-input:focus {
|
.sidebar-create-folder-input:focus {
|
||||||
@@ -702,24 +817,30 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-folder-btn:hover {
|
.sidebar-create-folder-btn:hover,
|
||||||
|
.sidebar-create-folder-btn:focus-visible {
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-folder-confirm:hover {
|
.sidebar-create-folder-confirm:hover,
|
||||||
|
.sidebar-create-folder-confirm:focus-visible {
|
||||||
background: oklch(from var(--success-color) l c h / 0.15);
|
background: oklch(from var(--success-color) l c h / 0.15);
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-folder-cancel:hover {
|
.sidebar-create-folder-cancel:hover,
|
||||||
|
.sidebar-create-folder-cancel:focus-visible {
|
||||||
background: oklch(from var(--error-color) l c h / 0.15);
|
background: oklch(from var(--error-color) l c h / 0.15);
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-create-folder-hint {
|
.sidebar-create-folder-hint {
|
||||||
|
|||||||
@@ -17,13 +17,13 @@
|
|||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card:hover {
|
.metric-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-elevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-card .metric-icon {
|
.metric-card .metric-icon {
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-item:hover {
|
.model-item:hover {
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
border: 1px solid oklch(var(--lora-accent) / 0.2);
|
border: 1px solid oklch(var(--lora-accent) / 0.2);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,12 +349,12 @@
|
|||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-card:hover {
|
.insight-card:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-card.type-success {
|
.insight-card.type-success {
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.recommendation-item:hover {
|
.recommendation-item:hover {
|
||||||
@@ -534,9 +534,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .metric-card {
|
[data-theme="dark"] .metric-card {
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .metric-card:hover {
|
[data-theme="dark"] .metric-card:hover {
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
box-shadow: var(--shadow-dark-lg);
|
||||||
}
|
}
|
||||||
@@ -15,18 +15,18 @@
|
|||||||
/* Toast Notifications */
|
/* Toast Notifications */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px; /* 改为从顶部显示 */
|
top: 20px;
|
||||||
right: 20px; /* 改为右对齐 */
|
right: 20px;
|
||||||
left: auto; /* 移除左对齐 */
|
left: auto;
|
||||||
transform: translateX(120%); /* 初始位置在屏幕右侧外 */
|
transform: translateX(120%);
|
||||||
min-width: 300px; /* 设置最小宽度 */
|
min-width: 300px;
|
||||||
max-width: 400px; /* 设置最大宽度 */
|
max-width: 400px;
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow-toast);
|
||||||
z-index: calc(var(--z-overlay) + 10); /* 让toast显示在最上层 */
|
z-index: calc(var(--z-overlay) + 10);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
@@ -36,11 +36,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toast.show {
|
.toast.show {
|
||||||
transform: translateX(0); /* 显示时滑入到正确位置 */
|
transform: translateX(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 添加图标容器 */
|
|
||||||
.toast::before {
|
.toast::before {
|
||||||
content: '';
|
content: '';
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -51,7 +50,7 @@
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 不同类型的toast样式 */
|
/* Toast type variants */
|
||||||
.toast-success {
|
.toast-success {
|
||||||
border-left: 4px solid oklch(65% 0.2 142);
|
border-left: 4px solid oklch(65% 0.2 142);
|
||||||
}
|
}
|
||||||
@@ -76,15 +75,15 @@
|
|||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232196f3'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%232196f3'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 多个toast堆叠显示 */
|
/* Stacked toast spacing */
|
||||||
.toast + .toast {
|
.toast + .toast {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.toast {
|
.toast {
|
||||||
width: calc(100% - 40px); /* 左右各留20px间距 */
|
width: calc(100% - 40px);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 15px;
|
padding: 0 var(--space-2);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: var(--z-base);
|
z-index: var(--z-base);
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
z-index: calc(var(--z-header) - 1);
|
z-index: calc(var(--z-header) - 1);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
padding: var(--space-1) 0;
|
padding: var(--space-1) 0;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive container for larger screens */
|
/* Responsive container for larger screens */
|
||||||
@@ -78,21 +78,23 @@
|
|||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button:hover {
|
.control-group button:hover,
|
||||||
|
.control-group button:focus-visible {
|
||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-lg);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button:active {
|
.control-group button:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button i {
|
.control-group button i {
|
||||||
@@ -100,7 +102,8 @@
|
|||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button:hover i {
|
.control-group button:hover i,
|
||||||
|
.control-group button:focus-visible i {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +134,7 @@
|
|||||||
|
|
||||||
.control-group button.favorite-filter i {
|
.control-group button.favorite-filter i {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
color: #ffc107;
|
color: var(--favorite-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button.update-filter i {
|
.control-group button.update-filter i {
|
||||||
@@ -183,7 +186,7 @@
|
|||||||
color: var(--shortcut-text);
|
color: var(--shortcut-text);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group button:hover .shortcut-key {
|
.control-group button:hover .shortcut-key {
|
||||||
@@ -219,8 +222,8 @@
|
|||||||
background-position: right 6px center;
|
background-position: right 6px center;
|
||||||
background-size: 14px;
|
background-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: var(--shadow-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for optgroups */
|
/* Style for optgroups */
|
||||||
@@ -252,7 +255,7 @@
|
|||||||
border-color: var(--lora-accent);
|
border-color: var(--lora-accent);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-group select:focus {
|
.control-group select:focus {
|
||||||
@@ -292,9 +295,9 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
z-index: var(--z-overlay);
|
z-index: var(--z-overlay);
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-to-top.visible {
|
.back-to-top.visible {
|
||||||
@@ -307,7 +310,7 @@
|
|||||||
background: var(--lora-accent);
|
background: var(--lora-accent);
|
||||||
color: white;
|
color: white;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent text selection in control and header areas */
|
/* Prevent text selection in control and header areas */
|
||||||
@@ -336,7 +339,7 @@
|
|||||||
.dropdown-main {
|
.dropdown-main {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
border-right: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-toggle {
|
.dropdown-toggle {
|
||||||
@@ -364,7 +367,7 @@
|
|||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
box-shadow: var(--shadow-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-group.active .dropdown-menu {
|
.dropdown-group.active .dropdown-menu {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
z-index: calc(var(--z-overlay) + 1);
|
z-index: calc(var(--z-overlay) + 1);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: all 0.3s ease;
|
transition: var(--transition-slow);
|
||||||
/* Add glow effect */
|
/* Add glow effect */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 2px rgba(24, 144, 255, 0.3),
|
0 0 0 2px rgba(24, 144, 255, 0.3),
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
z-index: calc(var(--z-overlay) + 3);
|
z-index: calc(var(--z-overlay) + 3);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-2xl);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-btn:hover {
|
.onboarding-btn:hover {
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
min-width: 510px;
|
min-width: 510px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: var(--shadow-dark-lg);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: var(--transition-base);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 使用已有的loading-spinner样式 */
|
/* Reuse existing loading-spinner styles */
|
||||||
.initialization-notice .loading-spinner {
|
.initialization-notice .loading-spinner {
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|||||||
142
static/css/tokens/MIGRATION.md
Normal file
142
static/css/tokens/MIGRATION.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# Lora-Manager UI Token Migration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The design token system has been created in `static/css/tokens/`. `base.css` now imports the tokens and provides backward-compatible aliases for existing component CSS.
|
||||||
|
|
||||||
|
## Token Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `tokens/colors.css` | OKLch color primitives + semantic light/dark tokens |
|
||||||
|
| `tokens/typography.css` | Font stacks, type scale, weights, line heights |
|
||||||
|
| `tokens/spacing.css` | 4px-base grid with legacy aliases |
|
||||||
|
| `tokens/effects.css` | Border radius, shadows, transitions |
|
||||||
|
| `tokens/breakpoints.css` | Named breakpoint variables |
|
||||||
|
| `tokens/z-index.css` | Stacking context scale |
|
||||||
|
| `tokens/index.css` | Aggregator that imports all token files |
|
||||||
|
|
||||||
|
## Backward Compatibility
|
||||||
|
|
||||||
|
Old variable names in component CSS still work via aliases in `base.css`:
|
||||||
|
|
||||||
|
| Old Name | Maps To |
|
||||||
|
|----------|---------|
|
||||||
|
| `--bg-color` | `--bg-base` |
|
||||||
|
| `--text-color` | `--text-primary` |
|
||||||
|
| `--text-muted` | `--text-secondary` |
|
||||||
|
| `--card-bg` | `--surface-base` |
|
||||||
|
| `--border-color` | `--border-base` |
|
||||||
|
| `--lora-accent` | `--color-accent` |
|
||||||
|
| `--lora-surface` | `--bg-elevated` |
|
||||||
|
| `--lora-border` | `--border-subtle` |
|
||||||
|
| `--space-1` (8px) | `--space-1-legacy` |
|
||||||
|
| `--border-radius-base` | `--radius-lg` |
|
||||||
|
|
||||||
|
## Phase 2: Component Audit Checklist
|
||||||
|
|
||||||
|
Below are the hardcoded values found across component CSS that should be replaced with tokens.
|
||||||
|
|
||||||
|
### Critical Fixes (P0)
|
||||||
|
|
||||||
|
- [ ] **card.css line 441**: `.base-model { background: #f0f0f0; }` → use `--bg-hover` or new `--surface-variant`
|
||||||
|
- [ ] **card.css line 369**: `.favorite-active { color: #ffc107 !important; }` → use `--favorite-color` (already defined in tokens)
|
||||||
|
- [ ] **layout.css line 134**: `.control-group button.favorite-filter i { color: #ffc107; }` → use `--favorite-color`
|
||||||
|
- [ ] **header.css lines 233-250**: Hardcoded dark theme colors (`#3a3a3a`, `#888888`, `#555555`) → use `--bg-disabled`, `--text-secondary`, `--border-base`
|
||||||
|
|
||||||
|
### Spacing Normalization (P1)
|
||||||
|
|
||||||
|
Replace hard pixel values with token equivalents:
|
||||||
|
|
||||||
|
- [ ] `padding: 4px 10px` → `padding: var(--space-1) var(--space-3)`
|
||||||
|
- [ ] `gap: 6px` → `gap: var(--space-1-legacy)` or `gap: var(--space-2)`
|
||||||
|
- [ ] `gap: 8px` → `gap: var(--space-2)`
|
||||||
|
- [ ] `gap: 12px` → `gap: var(--space-3)`
|
||||||
|
- [ ] `padding: 15px` → `padding: var(--space-4)`
|
||||||
|
- [ ] `padding: 16px` → `padding: var(--space-4)`
|
||||||
|
- [ ] `margin-top: 2px` → `margin-top: var(--space-0-5)`
|
||||||
|
- [ ] `padding: 2px 6px` → `padding: var(--space-0-5) var(--space-2)`
|
||||||
|
- [ ] `border-radius: 50%` → `border-radius: var(--radius-full)`
|
||||||
|
|
||||||
|
### Shadow Standardization (P1)
|
||||||
|
|
||||||
|
Replace hardcoded shadows with token equivalents:
|
||||||
|
|
||||||
|
- [ ] `box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05)` → `box-shadow: var(--shadow-xs)`
|
||||||
|
- [ ] `box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05)` → `box-shadow: var(--shadow-sm)`
|
||||||
|
- [ ] `box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)` → `box-shadow: var(--shadow-md)`
|
||||||
|
- [ ] `box-shadow: 0 3px 5px rgba(0, 0, 0, 0.08)` → `box-shadow: var(--shadow-lg)`
|
||||||
|
- [ ] `box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15)` → `box-shadow: var(--shadow-xl)`
|
||||||
|
- [ ] `box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08)` → combine or add new token
|
||||||
|
|
||||||
|
### Typography Normalization (P1)
|
||||||
|
|
||||||
|
Replace scattered font sizes with type scale:
|
||||||
|
|
||||||
|
- [ ] `font-size: 0.8em` → `font-size: var(--text-xs)`
|
||||||
|
- [ ] `font-size: 0.85em` → `font-size: var(--text-sm)`
|
||||||
|
- [ ] `font-size: 0.9em` → `font-size: var(--text-sm)`
|
||||||
|
- [ ] `font-size: 0.95em` → `font-size: var(--text-md)`
|
||||||
|
- [ ] `font-size: 1.1em` → `font-size: var(--text-lg)`
|
||||||
|
- [ ] `font-size: 11px` → `font-size: var(--text-xs)`
|
||||||
|
|
||||||
|
### Breakpoint Normalization (P2)
|
||||||
|
|
||||||
|
Replace magic numbers with named breakpoints:
|
||||||
|
|
||||||
|
- [ ] `@media (min-width: 2150px)` → `@media (min-width: var(--bp-ultrawide))`
|
||||||
|
- [ ] `@media (min-width: 3000px)` → `@media (min-width: var(--bp-4k))`
|
||||||
|
- [ ] `@media (max-width: 768px)` → `@media (max-width: var(--bp-mobile))`
|
||||||
|
- [ ] `@media (max-width: 1200px)` → `@media (max-width: var(--bp-desktop))`
|
||||||
|
|
||||||
|
### Z-Index Cleanup (P2)
|
||||||
|
|
||||||
|
Replace magic z-index values with tokens:
|
||||||
|
|
||||||
|
- [ ] `z-index: 2` / `z-index: 3` / `z-index: 4` in card.css → use `--z-base` + calc
|
||||||
|
- [ ] `z-index: 200` in header.css (hamburger dropdown) → use `--z-dropdown`
|
||||||
|
|
||||||
|
### Remaining Hardcoded Colors (P2)
|
||||||
|
|
||||||
|
- [ ] `rgba(0, 184, 122, 0.05)` and `#00B87A` in import-modal.css → use `--color-success`
|
||||||
|
- [ ] `rgba(255, 255, 255, 0.12)` in card.css (base-model-label background) → use token
|
||||||
|
- [ ] `rgba(255, 255, 255, 0.25)` in card.css (separator) → use `--border-inverse`
|
||||||
|
- [ ] `rgba(0, 0, 0, 0.5)` and `rgba(0, 0, 0, 0.7)` in card.css (toggle blur btn) → use `--bg-overlay` variants
|
||||||
|
- [ ] `rgba(46, 204, 113, 0.3)` and `rgba(231, 76, 60, 0.3)` in card.css → use success/error tokens
|
||||||
|
|
||||||
|
## New Tokens Added
|
||||||
|
|
||||||
|
The following tokens were added beyond the existing system:
|
||||||
|
|
||||||
|
| Token | Value | Use Case |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| `--color-accent-hover` | oklch(58% 0.28 256) | Hover states for accent buttons |
|
||||||
|
| `--color-accent-subtle` | accent @ 12% opacity | Subtle accent backgrounds |
|
||||||
|
| `--color-accent-border` | accent @ 25% opacity | Accent borders |
|
||||||
|
| `--color-accent-transparent` | accent @ 60% opacity | Glow effects, pulse animations |
|
||||||
|
| `--bg-hover` | oklch(95% 0.02 256) / dark: oklch(35% 0.02 256) | Hover backgrounds |
|
||||||
|
| `--bg-disabled` | #f5f5f5 / dark: #3a3a3a | Disabled input backgrounds |
|
||||||
|
| `--bg-overlay` | oklch(0% 0 0 / 0.75) | Modal overlays, gradients |
|
||||||
|
| `--surface-hover` | oklch(95% 0.02 256) / dark: oklch(35% 0.02 256) | Card/panel hover |
|
||||||
|
| `--favorite-color` | #d4a017 | Accessible gold for favorites |
|
||||||
|
| `--shadow-focus` | 0 0 0 1px accent | Focus ring shadow |
|
||||||
|
| `--shadow-glow` | 0 2px 6px info-glow | Badge glow effects |
|
||||||
|
| `--transition-bounce` | 200ms cubic-bezier | Playful hover transitions |
|
||||||
|
|
||||||
|
## Migration Order Recommendation
|
||||||
|
|
||||||
|
1. **Start with colors**: Replace `#ffc107` and `#f0f0f0` (highest visual impact)
|
||||||
|
2. **Then spacing**: Unify padding/gap values (biggest consistency win)
|
||||||
|
3. **Then shadows**: Replace rgba shadows with tokens
|
||||||
|
4. **Then typography**: Standardize font sizes
|
||||||
|
5. **Finally breakpoints + z-index**: Lower priority but good for maintainability
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
After each component file is migrated:
|
||||||
|
|
||||||
|
- [ ] Light theme renders correctly
|
||||||
|
- [ ] Dark theme renders correctly
|
||||||
|
- [ ] No visual regressions in card grid, header, modals
|
||||||
|
- [ ] Focus states still visible
|
||||||
|
- [ ] Hover transitions still work (unless prefers-reduced-motion)
|
||||||
8
static/css/tokens/breakpoints.css
Normal file
8
static/css/tokens/breakpoints.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
:root {
|
||||||
|
--bp-mobile: 768px;
|
||||||
|
--bp-tablet: 1024px;
|
||||||
|
--bp-desktop: 1400px;
|
||||||
|
--bp-wide: 1920px;
|
||||||
|
--bp-ultrawide: 2150px;
|
||||||
|
--bp-4k: 3000px;
|
||||||
|
}
|
||||||
117
static/css/tokens/colors.css
Normal file
117
static/css/tokens/colors.css
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
:root {
|
||||||
|
--color-accent-l: 68%;
|
||||||
|
--color-accent-c: 0.28;
|
||||||
|
--color-accent-h: 256;
|
||||||
|
--color-warning-l: 75%;
|
||||||
|
--color-warning-c: 0.25;
|
||||||
|
--color-warning-h: 80;
|
||||||
|
--color-success-l: 70%;
|
||||||
|
--color-success-c: 0.2;
|
||||||
|
--color-success-h: 140;
|
||||||
|
--color-error-l: 75%;
|
||||||
|
--color-error-c: 0.32;
|
||||||
|
--color-error-h: 29;
|
||||||
|
--color-info-l: 72%;
|
||||||
|
--color-info-c: 0.2;
|
||||||
|
--color-info-h: 220;
|
||||||
|
--color-neutral-h: 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--color-accent: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
|
||||||
|
--color-accent-hover: oklch(58% var(--color-accent-c) var(--color-accent-h));
|
||||||
|
--color-accent-subtle: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.12);
|
||||||
|
--color-accent-border: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.25);
|
||||||
|
--color-accent-transparent: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.6);
|
||||||
|
|
||||||
|
--color-warning: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
|
||||||
|
--color-warning-bg: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h) / 0.15);
|
||||||
|
--color-warning-border: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h) / 0.3);
|
||||||
|
|
||||||
|
--color-success: oklch(var(--color-success-l) var(--color-success-c) var(--color-success-h));
|
||||||
|
--color-success-bg: oklch(var(--color-success-l) var(--color-success-c) var(--color-success-h) / 0.2);
|
||||||
|
--color-success-border: oklch(var(--color-success-l) var(--color-success-c) var(--color-success-h) / 0.3);
|
||||||
|
|
||||||
|
--color-error: oklch(var(--color-error-l) var(--color-error-c) var(--color-error-h));
|
||||||
|
--color-error-bg: color-mix(in oklch, var(--color-error) 20%, transparent);
|
||||||
|
--color-error-border: color-mix(in oklch, var(--color-error) 50%, transparent);
|
||||||
|
|
||||||
|
--color-info: oklch(var(--color-info-l) var(--color-info-c) var(--color-info-h));
|
||||||
|
--color-info-bg: oklch(72% 0.2 220);
|
||||||
|
--color-info-text: oklch(28% 0.03 220);
|
||||||
|
--color-info-glow: oklch(72% 0.2 220 / 0.28);
|
||||||
|
|
||||||
|
--color-skip-refresh-bg: oklch(82% 0.12 45);
|
||||||
|
--color-skip-refresh-text: oklch(35% 0.02 45);
|
||||||
|
--color-skip-refresh-glow: oklch(82% 0.12 45 / 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-base: #ffffff;
|
||||||
|
--bg-elevated: oklch(97% 0 0 / 0.95);
|
||||||
|
--bg-overlay: oklch(0% 0 0 / 0.75);
|
||||||
|
--bg-hover: oklch(95% 0.02 256);
|
||||||
|
--bg-disabled: #f5f5f5;
|
||||||
|
|
||||||
|
--text-primary: #333333;
|
||||||
|
--text-secondary: #6c757d;
|
||||||
|
--text-inverse: #ffffff;
|
||||||
|
--text-muted-on-dark: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
--surface-base: #ffffff;
|
||||||
|
--surface-elevated: oklch(97% 0 0 / 0.95);
|
||||||
|
--surface-hover: oklch(95% 0.02 256);
|
||||||
|
--surface-subtle: oklch(0% 0 0 / 0.03);
|
||||||
|
|
||||||
|
--border-base: #e0e0e0;
|
||||||
|
--border-subtle: oklch(72% 0.03 256 / 0.45);
|
||||||
|
--border-inverse: rgba(255, 255, 255, 0.25);
|
||||||
|
|
||||||
|
--status-success-text: oklch(75% 0.12 230);
|
||||||
|
--status-success-bg: oklch(55% 0.15 240 / 0.25);
|
||||||
|
--status-success-border: oklch(60% 0.18 250 / 0.3);
|
||||||
|
--status-info-text: oklch(78% 0.10 185);
|
||||||
|
--status-info-bg: oklch(50% 0.10 190 / 0.25);
|
||||||
|
--status-info-border: oklch(55% 0.12 195 / 0.3);
|
||||||
|
|
||||||
|
--favorite-color: #d4a017;
|
||||||
|
--favorite-glow: oklch(65% 0.15 85 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-base: #1a1a1a;
|
||||||
|
--bg-elevated: oklch(25% 0.02 256 / 0.98);
|
||||||
|
--bg-overlay: oklch(0% 0 0 / 0.75);
|
||||||
|
--bg-hover: oklch(35% 0.02 256);
|
||||||
|
--bg-disabled: #3a3a3a;
|
||||||
|
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-inverse: #1a1a1a;
|
||||||
|
--text-muted-on-dark: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
--surface-base: #2d2d2d;
|
||||||
|
--surface-elevated: oklch(25% 0.02 256 / 0.98);
|
||||||
|
--surface-hover: oklch(35% 0.02 256);
|
||||||
|
--surface-subtle: oklch(100% 0 0 / 0.03);
|
||||||
|
|
||||||
|
--border-base: #404040;
|
||||||
|
--border-subtle: oklch(90% 0.02 256 / 0.15);
|
||||||
|
--border-inverse: rgba(255, 255, 255, 0.25);
|
||||||
|
|
||||||
|
--status-success-text: oklch(75% 0.12 230);
|
||||||
|
--status-success-bg: oklch(55% 0.15 240 / 0.25);
|
||||||
|
--status-success-border: oklch(60% 0.18 250 / 0.3);
|
||||||
|
--status-info-text: oklch(78% 0.10 185);
|
||||||
|
--status-info-bg: oklch(50% 0.10 190 / 0.25);
|
||||||
|
--status-info-border: oklch(55% 0.12 195 / 0.3);
|
||||||
|
|
||||||
|
--color-info-bg: oklch(62% 0.18 220);
|
||||||
|
--color-info-text: oklch(98% 0.02 240);
|
||||||
|
--color-info-glow: oklch(62% 0.18 220 / 0.4);
|
||||||
|
|
||||||
|
--color-error-bg: color-mix(in oklch, var(--color-error) 15%, transparent);
|
||||||
|
--color-error-border: color-mix(in oklch, var(--color-error) 40%, transparent);
|
||||||
|
|
||||||
|
--favorite-color: #ffc107;
|
||||||
|
}
|
||||||
57
static/css/tokens/effects.css
Normal file
57
static/css/tokens/effects.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
:root {
|
||||||
|
--radius-none: 0px;
|
||||||
|
--radius-xs: 4px;
|
||||||
|
--radius-sm: 6px;
|
||||||
|
--radius-md: 8px;
|
||||||
|
--radius-lg: 12px;
|
||||||
|
--radius-xl: 16px;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
|
||||||
|
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 3px 5px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-xl: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-2xl: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
--shadow-focus: 0 0 0 1px var(--color-accent);
|
||||||
|
--shadow-glow: 0 2px 6px var(--color-info-glow);
|
||||||
|
|
||||||
|
--shadow-card: var(--shadow-sm);
|
||||||
|
--shadow-dropdown: var(--shadow-md);
|
||||||
|
--shadow-modal: var(--shadow-lg);
|
||||||
|
--shadow-toast: var(--shadow-xl);
|
||||||
|
--shadow-header: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-dark-lg: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-side: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-elevated: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-dialog: 0 10px 24px rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-inset-top: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 200ms ease;
|
||||||
|
--transition-slow: 300ms ease;
|
||||||
|
--transition-bounce: 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
|
--border-width-thin: 1px;
|
||||||
|
--border-width-thick: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-md: 0 2px 4px rgba(0, 0, 0, 0.35);
|
||||||
|
--shadow-lg: 0 3px 5px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-xl: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||||||
|
--shadow-2xl: 0 8px 32px rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
|
--shadow-card: var(--shadow-sm);
|
||||||
|
--shadow-dropdown: var(--shadow-md);
|
||||||
|
--shadow-modal: var(--shadow-lg);
|
||||||
|
--shadow-toast: var(--shadow-xl);
|
||||||
|
--shadow-header: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-dark-lg: 0 4px 24px rgba(0, 0, 0, 0.6);
|
||||||
|
--shadow-side: 2px 0 8px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-elevated: 0 4px 12px rgba(0, 0, 0, 0.35);
|
||||||
|
--shadow-dialog: 0 10px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
--shadow-inset-top: 0 -2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
6
static/css/tokens/index.css
Normal file
6
static/css/tokens/index.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@import 'colors.css';
|
||||||
|
@import 'typography.css';
|
||||||
|
@import 'spacing.css';
|
||||||
|
@import 'effects.css';
|
||||||
|
@import 'breakpoints.css';
|
||||||
|
@import 'z-index.css';
|
||||||
19
static/css/tokens/spacing.css
Normal file
19
static/css/tokens/spacing.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
:root {
|
||||||
|
--space-0-5: 2px;
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 32px;
|
||||||
|
--space-10: 40px;
|
||||||
|
--space-12: 48px;
|
||||||
|
--space-16: 64px;
|
||||||
|
--space-20: 80px;
|
||||||
|
|
||||||
|
--space-1-legacy: calc(8px * 1);
|
||||||
|
--space-2-legacy: calc(8px * 2);
|
||||||
|
--space-3-legacy: calc(8px * 3);
|
||||||
|
--space-4-legacy: calc(8px * 4);
|
||||||
|
}
|
||||||
23
static/css/tokens/typography.css
Normal file
23
static/css/tokens/typography.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
:root {
|
||||||
|
--font-display: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', system-ui, sans-serif;
|
||||||
|
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
|
||||||
|
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-base: 1rem;
|
||||||
|
--text-md: 0.95rem;
|
||||||
|
--text-lg: 1.1rem;
|
||||||
|
--text-xl: 1.25rem;
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-3xl: 2rem;
|
||||||
|
|
||||||
|
--leading-tight: 1.2;
|
||||||
|
--leading-normal: 1.4;
|
||||||
|
--leading-relaxed: 1.5;
|
||||||
|
|
||||||
|
--weight-normal: 400;
|
||||||
|
--weight-medium: 500;
|
||||||
|
--weight-semibold: 600;
|
||||||
|
--weight-bold: 700;
|
||||||
|
}
|
||||||
11
static/css/tokens/z-index.css
Normal file
11
static/css/tokens/z-index.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
:root {
|
||||||
|
--z-base: 10;
|
||||||
|
--z-sticky: 50;
|
||||||
|
--z-header: 100;
|
||||||
|
--z-dropdown: 200;
|
||||||
|
--z-modal-backdrop: 500;
|
||||||
|
--z-modal: 1000;
|
||||||
|
--z-overlay: 2000;
|
||||||
|
--z-toast: 3000;
|
||||||
|
--z-tooltip: 4000;
|
||||||
|
}
|
||||||
@@ -213,6 +213,8 @@ export async function resetAndReloadWithVirtualScroll(options = {}) {
|
|||||||
|
|
||||||
if (scrollSnapshot) {
|
if (scrollSnapshot) {
|
||||||
await restoreScrollPosition(scrollSnapshot);
|
await restoreScrollPosition(scrollSnapshot);
|
||||||
|
} else if (state.virtualScroller?.scrollContainer) {
|
||||||
|
state.virtualScroller.scrollContainer.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -42,27 +42,43 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||||
const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]');
|
const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]');
|
||||||
const repairMetadataItem = this.menu.querySelector('[data-action="repair-metadata"]');
|
const repairMetadataItem = this.menu.querySelector('[data-action="repair-metadata"]');
|
||||||
|
const reimportMetadataItem = this.menu.querySelector('[data-action="reimport-metadata"]');
|
||||||
|
|
||||||
if (repairMetadataItem) {
|
if (repairMetadataItem) {
|
||||||
repairMetadataItem.style.display = config.repairMetadata ? 'flex' : 'none';
|
repairMetadataItem.style.display = config.repairMetadata ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (reimportMetadataItem) {
|
||||||
|
reimportMetadataItem.style.display = config.reimportMetadata ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEmbeddings = currentModelType === 'embeddings';
|
||||||
if (sendToWorkflowAppendItem) {
|
if (sendToWorkflowAppendItem) {
|
||||||
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
if (sendToWorkflowReplaceItem) {
|
if (sendToWorkflowReplaceItem) {
|
||||||
sendToWorkflowReplaceItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
sendToWorkflowReplaceItem.style.display = (config.sendToWorkflow && !isEmbeddings) ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
if (copyAllItem) {
|
if (copyAllItem) {
|
||||||
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
|
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submenu parent visibility
|
// Submenu parent - for embeddings, collapse into a direct item (no replace choice)
|
||||||
const sendToWorkflowSubmenu = this.menu.querySelector('[data-has-submenu="send-to-workflow"]');
|
const sendToWorkflowSubmenu = this.menu.querySelector('[data-has-submenu="send-to-workflow"]');
|
||||||
if (sendToWorkflowSubmenu) {
|
if (sendToWorkflowSubmenu) {
|
||||||
const hasWorkflowActions = config.sendToWorkflow || config.copyAll;
|
const hasWorkflowActions = config.sendToWorkflow || config.copyAll;
|
||||||
|
if (isEmbeddings && config.sendToWorkflow && !config.copyAll) {
|
||||||
|
sendToWorkflowSubmenu.classList.remove('has-submenu');
|
||||||
|
sendToWorkflowSubmenu.removeAttribute('data-has-submenu');
|
||||||
|
sendToWorkflowSubmenu.dataset.action = 'send-to-workflow-append';
|
||||||
|
const arrow = sendToWorkflowSubmenu.querySelector('.submenu-arrow');
|
||||||
|
if (arrow) arrow.style.display = 'none';
|
||||||
|
const submenu = sendToWorkflowSubmenu.querySelector('.context-submenu');
|
||||||
|
if (submenu) submenu.style.display = 'none';
|
||||||
|
sendToWorkflowSubmenu.style.display = 'flex';
|
||||||
|
} else {
|
||||||
sendToWorkflowSubmenu.style.display = hasWorkflowActions ? 'flex' : 'none';
|
sendToWorkflowSubmenu.style.display = hasWorkflowActions ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (refreshAllItem) {
|
if (refreshAllItem) {
|
||||||
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||||
@@ -264,6 +280,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
case 'repair-metadata':
|
case 'repair-metadata':
|
||||||
bulkManager.repairSelectedRecipes();
|
bulkManager.repairSelectedRecipes();
|
||||||
break;
|
break;
|
||||||
|
case 'reimport-metadata':
|
||||||
|
bulkManager.reimportSelectedRecipes();
|
||||||
|
break;
|
||||||
case 'set-favorite': {
|
case 'set-favorite': {
|
||||||
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
|
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
|
||||||
bulkManager.setBulkFavorites(!allFavorited);
|
bulkManager.setBulkFavorites(!allFavorited);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
|||||||
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||||
import { moveManager } from '../../managers/MoveManager.js';
|
import { moveManager } from '../../managers/MoveManager.js';
|
||||||
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
|
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
|
||||||
|
import { sendEmbeddingToWorkflow } from '../../utils/uiHelpers.js';
|
||||||
|
|
||||||
export class EmbeddingContextMenu extends BaseContextMenu {
|
export class EmbeddingContextMenu extends BaseContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -51,6 +52,13 @@ export class EmbeddingContextMenu extends BaseContextMenu {
|
|||||||
this.currentCard.querySelector('.fa-copy').click();
|
this.currentCard.querySelector('.fa-copy').click();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'sendtoworkflow': {
|
||||||
|
const folder = this.currentCard.dataset.folder || '';
|
||||||
|
const name = this.currentCard.dataset.file_name || '';
|
||||||
|
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
|
sendEmbeddingToWorkflow(embeddingCode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'refresh-metadata':
|
case 'refresh-metadata':
|
||||||
// Refresh metadata from CivitAI
|
// Refresh metadata from CivitAI
|
||||||
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
|
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export class RecipeContextMenu extends BaseContextMenu {
|
|||||||
// Repair recipe metadata
|
// Repair recipe metadata
|
||||||
this.repairRecipe(recipeId);
|
this.repairRecipe(recipeId);
|
||||||
break;
|
break;
|
||||||
|
case 'reimport':
|
||||||
|
this.reimportRecipe(recipeId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +328,35 @@ export class RecipeContextMenu extends BaseContextMenu {
|
|||||||
showToast('recipes.contextMenu.repair.failed', { message: error.message }, 'error');
|
showToast('recipes.contextMenu.repair.failed', { message: error.message }, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reimportRecipe(recipeId) {
|
||||||
|
if (!recipeId) {
|
||||||
|
showToast('recipes.contextMenu.reimport.missingId', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.loadingManager.showSimpleLoading('Re-importing recipe from source...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/lm/recipe/${recipeId}/reimport`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
showToast('toast.recipes.reimportSuccess', {}, 'success');
|
||||||
|
const { resetAndReload } = await import('../../api/recipeApi.js');
|
||||||
|
resetAndReload(false, { preserveScroll: false });
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Re-import failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reimporting recipe:', error);
|
||||||
|
state.loadingManager.hide();
|
||||||
|
showToast('recipes.contextMenu.reimport.failed', { message: error.message }, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mix in shared methods from ModelContextMenuMixin
|
// Mix in shared methods from ModelContextMenuMixin
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export class SidebarManager {
|
|||||||
this.currentDropTarget = null;
|
this.currentDropTarget = null;
|
||||||
this.lastPageControls = null;
|
this.lastPageControls = null;
|
||||||
this.isDisabledBySetting = false;
|
this.isDisabledBySetting = false;
|
||||||
|
this.isDisabledByPage = false;
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
this.isCreatingFolder = false;
|
this.isCreatingFolder = false;
|
||||||
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
|
this._pendingDragState = null; // 用于保存拖拽创建文件夹时的状态
|
||||||
@@ -68,6 +70,10 @@ export class SidebarManager {
|
|||||||
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
|
this.handleSidebarDrop = this.handleSidebarDrop.bind(this);
|
||||||
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
|
this.handleCreateFolderSubmit = this.handleCreateFolderSubmit.bind(this);
|
||||||
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
|
this.handleCreateFolderCancel = this.handleCreateFolderCancel.bind(this);
|
||||||
|
this.handleMoreToggle = this.handleMoreToggle.bind(this);
|
||||||
|
this.handleMoreDropdownItemClick = this.handleMoreDropdownItemClick.bind(this);
|
||||||
|
this.handleDocumentClickForMore = this.handleDocumentClickForMore.bind(this);
|
||||||
|
this.getPageDisplayName = this.getPageDisplayName.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
setHostPageControls(pageControls) {
|
setHostPageControls(pageControls) {
|
||||||
@@ -100,6 +106,8 @@ export class SidebarManager {
|
|||||||
this.initializeDragAndDrop();
|
this.initializeDragAndDrop();
|
||||||
this.updateSidebarTitle();
|
this.updateSidebarTitle();
|
||||||
this.restoreSidebarState();
|
this.restoreSidebarState();
|
||||||
|
// Re-apply DOM visibility now that per-page state is known
|
||||||
|
this.updateDomVisibility(!this.isDisabledBySetting);
|
||||||
await this.loadFolderTree();
|
await this.loadFolderTree();
|
||||||
if (this.isDisabledBySetting && !forceInitialize) {
|
if (this.isDisabledBySetting && !forceInitialize) {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@@ -143,6 +151,13 @@ export class SidebarManager {
|
|||||||
this.sidebarDragHandlersInitialized = false;
|
this.sidebarDragHandlersInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.classList.remove('open');
|
||||||
|
}
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
this.pageControls = null;
|
this.pageControls = null;
|
||||||
this.pageType = null;
|
this.pageType = null;
|
||||||
@@ -151,6 +166,7 @@ export class SidebarManager {
|
|||||||
this.expandedNodes = new Set();
|
this.expandedNodes = new Set();
|
||||||
this.openDropdown = null;
|
this.openDropdown = null;
|
||||||
this.isHovering = false;
|
this.isHovering = false;
|
||||||
|
this.isDisabledByPage = false;
|
||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.recursiveSearchEnabled = true;
|
this.recursiveSearchEnabled = true;
|
||||||
@@ -217,6 +233,18 @@ export class SidebarManager {
|
|||||||
if (recursiveToggleBtn) {
|
if (recursiveToggleBtn) {
|
||||||
recursiveToggleBtn.removeEventListener('click', this.handleRecursiveToggle);
|
recursiveToggleBtn.removeEventListener('click', this.handleRecursiveToggle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const moreToggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (moreToggle) {
|
||||||
|
moreToggle.removeEventListener('click', this.handleMoreToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.removeEventListener('click', this.handleMoreDropdownItemClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('click', this.handleDocumentClickForMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDragAndDrop() {
|
initializeDragAndDrop() {
|
||||||
@@ -1045,6 +1073,19 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// More options dropdown
|
||||||
|
const moreToggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (moreToggle) {
|
||||||
|
moreToggle.addEventListener('click', this.handleMoreToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moreDropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (moreDropdown) {
|
||||||
|
moreDropdown.addEventListener('click', this.handleMoreDropdownItemClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', this.handleDocumentClickForMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentClick(event) {
|
handleDocumentClick(event) {
|
||||||
@@ -1066,6 +1107,7 @@ export class SidebarManager {
|
|||||||
this.isPinned = !this.isPinned;
|
this.isPinned = !this.isPinned;
|
||||||
this.updateAutoHideState();
|
this.updateAutoHideState();
|
||||||
this.updatePinButton();
|
this.updatePinButton();
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
this.saveSidebarState();
|
this.saveSidebarState();
|
||||||
this.updateContainerMargin();
|
this.updateContainerMargin();
|
||||||
}
|
}
|
||||||
@@ -1129,7 +1171,7 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAutoHideState() {
|
updateAutoHideState() {
|
||||||
if (this.isDisabledBySetting) return;
|
if (this.isDisabledBySetting || this.isDisabledByPage) return;
|
||||||
|
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
@@ -1174,9 +1216,12 @@ export class SidebarManager {
|
|||||||
|
|
||||||
if (!container || !sidebar || this.isDisabledBySetting) return;
|
if (!container || !sidebar || this.isDisabledBySetting) return;
|
||||||
|
|
||||||
// Reset margin to default
|
// Always reset margin first — needed when transitioning from visible to hidden
|
||||||
container.style.marginLeft = '';
|
container.style.marginLeft = '';
|
||||||
|
|
||||||
|
// When per-page disabled, skip adjustment but margin is already reset
|
||||||
|
if (this.isDisabledByPage) return;
|
||||||
|
|
||||||
// Only adjust margin if sidebar is visible and pinned
|
// Only adjust margin if sidebar is visible and pinned
|
||||||
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
if ((this.isPinned || this.isHovering) && this.isVisible) {
|
||||||
const sidebarWidth = sidebar.offsetWidth;
|
const sidebarWidth = sidebar.offsetWidth;
|
||||||
@@ -1193,20 +1238,29 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDomVisibility(enabled) {
|
updateDomVisibility(enabled) {
|
||||||
|
// Per-page disable adds on top of global setting
|
||||||
|
const isVisible = enabled && !this.isDisabledByPage;
|
||||||
const sidebar = document.getElementById('folderSidebar');
|
const sidebar = document.getElementById('folderSidebar');
|
||||||
const hoverArea = document.getElementById('sidebarHoverArea');
|
const hoverArea = document.getElementById('sidebarHoverArea');
|
||||||
|
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.toggle('hidden-by-setting', !enabled);
|
sidebar.classList.toggle('hidden-by-setting', !isVisible);
|
||||||
sidebar.setAttribute('aria-hidden', (!enabled).toString());
|
sidebar.setAttribute('aria-hidden', (!isVisible).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hoverArea) {
|
if (hoverArea) {
|
||||||
hoverArea.classList.toggle('hidden-by-setting', !enabled);
|
hoverArea.classList.toggle('hidden-by-setting', !isVisible);
|
||||||
if (!enabled) {
|
if (!isVisible) {
|
||||||
hoverArea.classList.add('disabled');
|
hoverArea.classList.add('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show or hide the "sidebar hidden" notification
|
||||||
|
if (enabled && this.isDisabledByPage) {
|
||||||
|
this.showSidebarHiddenIndicator();
|
||||||
|
} else {
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setSidebarEnabled(enabled) {
|
async setSidebarEnabled(enabled) {
|
||||||
@@ -1266,6 +1320,133 @@ export class SidebarManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== More Options Dropdown =====
|
||||||
|
|
||||||
|
handleMoreToggle(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
this.isMoreDropdownOpen = !dropdown.classList.contains('open');
|
||||||
|
dropdown.classList.toggle('open', this.isMoreDropdownOpen);
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMoreDropdownItemClick(event) {
|
||||||
|
const item = event.target.closest('.sidebar-dropdown-item');
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const action = item.dataset.action;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'toggle-pin':
|
||||||
|
this.handlePinToggle(event);
|
||||||
|
break;
|
||||||
|
case 'toggle-hide':
|
||||||
|
this.toggleHideOnThisPage();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDocumentClickForMore(event) {
|
||||||
|
const dropdown = document.getElementById('sidebarMoreDropdown');
|
||||||
|
const toggle = document.getElementById('sidebarMoreToggle');
|
||||||
|
if (!dropdown || !toggle) return;
|
||||||
|
|
||||||
|
if (!dropdown.contains(event.target) && !toggle.contains(event.target)) {
|
||||||
|
dropdown.classList.remove('open');
|
||||||
|
this.isMoreDropdownOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMoreDropdownLabels() {
|
||||||
|
const pinLabel = document.getElementById('sidebarMorePinLabel');
|
||||||
|
if (pinLabel) {
|
||||||
|
pinLabel.textContent = this.isPinned
|
||||||
|
? translate('sidebar.unpinSidebar')
|
||||||
|
: translate('sidebar.pinSidebar');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideItem = document.querySelector('.sidebar-dropdown-item[data-action="toggle-hide"]');
|
||||||
|
if (hideItem) {
|
||||||
|
const hideIcon = hideItem.querySelector('i');
|
||||||
|
const hideLabel = hideItem.querySelector('span');
|
||||||
|
if (this.isDisabledByPage) {
|
||||||
|
hideLabel.textContent = translate('sidebar.showSidebar');
|
||||||
|
if (hideIcon) {
|
||||||
|
hideIcon.className = 'fas fa-eye';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hideLabel.textContent = translate('sidebar.hideOnThisPage');
|
||||||
|
if (hideIcon) {
|
||||||
|
hideIcon.className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleHideOnThisPage() {
|
||||||
|
this.isDisabledByPage = !this.isDisabledByPage;
|
||||||
|
setStorageItem(`${this.pageType}_sidebarDisabled`, this.isDisabledByPage);
|
||||||
|
this.updateDomVisibility(!this.isDisabledBySetting);
|
||||||
|
this.updateAutoHideState();
|
||||||
|
this.updateContainerMargin();
|
||||||
|
this.updateMoreDropdownLabels();
|
||||||
|
|
||||||
|
if (!this.isDisabledByPage) {
|
||||||
|
this.hideSidebarHiddenIndicator();
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
'sidebar.sidebarHiddenNotification',
|
||||||
|
{ page: this.getPageDisplayName() },
|
||||||
|
'info',
|
||||||
|
`Sidebar hidden on ${this.getPageDisplayName()} page`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageDisplayName() {
|
||||||
|
const names = {
|
||||||
|
loras: 'LoRAs',
|
||||||
|
recipes: 'Recipes',
|
||||||
|
checkpoints: 'Checkpoints',
|
||||||
|
embeddings: 'Embeddings',
|
||||||
|
};
|
||||||
|
return names[this.pageType] || this.pageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
showSidebarHiddenIndicator() {
|
||||||
|
if (document.getElementById('sidebarHiddenIndicator')) return;
|
||||||
|
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.id = 'sidebarHiddenIndicator';
|
||||||
|
indicator.className = 'sidebar-hidden-indicator';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
<span class="sidebar-hidden-indicator-tooltip">${translate('sidebar.showSidebar')}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
indicator.addEventListener('click', () => {
|
||||||
|
this.toggleHideOnThisPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSidebarHiddenIndicator() {
|
||||||
|
const indicator = document.getElementById('sidebarHiddenIndicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadFolderTree() {
|
async loadFolderTree() {
|
||||||
try {
|
try {
|
||||||
if (this.displayMode === 'tree') {
|
if (this.displayMode === 'tree') {
|
||||||
@@ -1911,6 +2092,7 @@ export class SidebarManager {
|
|||||||
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []);
|
||||||
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree'
|
||||||
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
const recursiveSearchEnabled = getStorageItem(`${this.pageType}_recursiveSearch`, true);
|
||||||
|
this.isDisabledByPage = getStorageItem(`${this.pageType}_sidebarDisabled`, false);
|
||||||
|
|
||||||
this.isPinned = isPinned;
|
this.isPinned = isPinned;
|
||||||
this.expandedNodes = new Set(expandedPaths);
|
this.expandedNodes = new Set(expandedPaths);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js';
|
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, sendEmbeddingToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js';
|
||||||
import { state, getCurrentPageState } from '../../state/index.js';
|
import { state, getCurrentPageState } from '../../state/index.js';
|
||||||
import { showModelModal } from './ModelModal.js';
|
import { showModelModal } from './ModelModal.js';
|
||||||
import { toggleShowcase } from './showcase/ShowcaseView.js';
|
import { toggleShowcase } from './showcase/ShowcaseView.js';
|
||||||
@@ -216,6 +216,11 @@ function handleSendToWorkflow(card, replaceMode, modelType) {
|
|||||||
missingNodesMessage,
|
missingNodesMessage,
|
||||||
missingTargetMessage,
|
missingTargetMessage,
|
||||||
});
|
});
|
||||||
|
} else if (modelType === MODEL_TYPES.EMBEDDING) {
|
||||||
|
const folder = card.dataset.folder || '';
|
||||||
|
const name = card.dataset.file_name || '';
|
||||||
|
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
|
sendEmbeddingToWorkflow(embeddingCode);
|
||||||
} else {
|
} else {
|
||||||
showToast('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'info');
|
showToast('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'info');
|
||||||
}
|
}
|
||||||
@@ -230,8 +235,11 @@ function handleCopyAction(card, modelType) {
|
|||||||
const message = translate('modelCard.actions.checkpointNameCopied', {}, 'Checkpoint name copied');
|
const message = translate('modelCard.actions.checkpointNameCopied', {}, 'Checkpoint name copied');
|
||||||
copyToClipboard(checkpointName, message);
|
copyToClipboard(checkpointName, message);
|
||||||
} else if (modelType === MODEL_TYPES.EMBEDDING) {
|
} else if (modelType === MODEL_TYPES.EMBEDDING) {
|
||||||
const embeddingName = card.dataset.file_name;
|
const folder = card.dataset.folder || '';
|
||||||
copyToClipboard(embeddingName, 'Embedding name copied');
|
const name = card.dataset.file_name || '';
|
||||||
|
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
|
const message = translate('modelCard.actions.embeddingNameCopied', {}, 'Embedding syntax copied');
|
||||||
|
copyToClipboard(embeddingCode, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { showToast, openCivitai, sendLoraToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
|
import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
|
||||||
import { modalManager } from '../../managers/ModalManager.js';
|
import { modalManager } from '../../managers/ModalManager.js';
|
||||||
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
||||||
import {
|
import {
|
||||||
@@ -510,7 +510,12 @@ export async function showModelModal(model, modelType) {
|
|||||||
</div>
|
</div>
|
||||||
${typeSpecificContent}
|
${typeSpecificContent}
|
||||||
<div class="info-item notes">
|
<div class="info-item notes">
|
||||||
|
<div class="notes-header">
|
||||||
<label>${translate('modals.model.metadata.additionalNotes', {}, 'Additional Notes')} <i class="fas fa-info-circle notes-hint" title="${translate('modals.model.metadata.notesHint', {}, 'Press Enter to save, Shift+Enter for new line')}"></i></label>
|
<label>${translate('modals.model.metadata.additionalNotes', {}, 'Additional Notes')} <i class="fas fa-info-circle notes-hint" title="${translate('modals.model.metadata.notesHint', {}, 'Press Enter to save, Shift+Enter for new line')}"></i></label>
|
||||||
|
<button class="notes-toggle-btn" style="display:none" title="${translate('modals.model.notes.showMore', {}, 'Show more')}">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="editable-field">
|
<div class="editable-field">
|
||||||
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || translate('modals.model.metadata.addNotesPlaceholder', {}, 'Add your notes here...')}</div>
|
<div class="notes-content" contenteditable="true" spellcheck="false">${modelWithFullData.notes || translate('modals.model.metadata.addNotesPlaceholder', {}, 'Add your notes here...')}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -643,6 +648,10 @@ export async function showModelModal(model, modelType) {
|
|||||||
if (modelType === 'checkpoints' && modelWithFullData.sub_type) {
|
if (modelType === 'checkpoints' && modelWithFullData.sub_type) {
|
||||||
activeModalElement.dataset.subType = modelWithFullData.sub_type;
|
activeModalElement.dataset.subType = modelWithFullData.sub_type;
|
||||||
}
|
}
|
||||||
|
// Store folder for embedding models
|
||||||
|
if (modelType === 'embeddings' && modelWithFullData.folder) {
|
||||||
|
activeModalElement.dataset.folder = modelWithFullData.folder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updateVersionsTabBadge(updateAvailabilityState.hasUpdateAvailable);
|
updateVersionsTabBadge(updateAvailabilityState.hasUpdateAvailable);
|
||||||
const versionsTabController = initVersionsTab({
|
const versionsTabController = initVersionsTab({
|
||||||
@@ -837,12 +846,70 @@ function setupEditableFields(filePath, modelType) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup adaptive expand/collapse for notes
|
||||||
|
setupNotesExpand();
|
||||||
|
|
||||||
// LoRA specific field setup
|
// LoRA specific field setup
|
||||||
if (modelType === 'loras') {
|
if (modelType === 'loras') {
|
||||||
setupLoraSpecificFields(filePath);
|
setupLoraSpecificFields(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptive expand/collapse for the Additional Notes section.
|
||||||
|
* Measures content height synchronously after render (before first paint,
|
||||||
|
* so no visual flash). If notes fit within ~4 lines, no toggle is shown.
|
||||||
|
* If they exceed the threshold, the field collapses with a gradient fade
|
||||||
|
* and a "Show more" button appears.
|
||||||
|
*/
|
||||||
|
function setupNotesExpand() {
|
||||||
|
const notesContainer = document.querySelector('.info-item.notes');
|
||||||
|
if (!notesContainer) return;
|
||||||
|
|
||||||
|
const notesField = notesContainer.querySelector('.editable-field');
|
||||||
|
const notesContent = notesContainer.querySelector('.notes-content');
|
||||||
|
const toggleBtn = notesContainer.querySelector('.notes-toggle-btn');
|
||||||
|
|
||||||
|
if (!notesField || !notesContent || !toggleBtn) return;
|
||||||
|
|
||||||
|
const placeholderText = translate('modals.model.metadata.addNotesPlaceholder', {}, 'Add your notes here...');
|
||||||
|
const content = notesContent.textContent || '';
|
||||||
|
const isEmpty = !content.trim() || content === placeholderText;
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS default has no constraints, so scrollHeight reflects full content
|
||||||
|
const contentHeight = notesContent.scrollHeight;
|
||||||
|
const collapsedThreshold = 95; // ~4 lines
|
||||||
|
|
||||||
|
if (contentHeight <= collapsedThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Long content — collapse and show toggle
|
||||||
|
notesField.classList.add('collapsed');
|
||||||
|
toggleBtn.style.display = 'inline-flex';
|
||||||
|
toggleBtn.title = translate('modals.model.notes.showMore', {}, 'Show more');
|
||||||
|
|
||||||
|
const toggleIcon = toggleBtn.querySelector('i');
|
||||||
|
|
||||||
|
toggleBtn.addEventListener('click', function onClick() {
|
||||||
|
const isCollapsed = notesField.classList.contains('collapsed');
|
||||||
|
if (isCollapsed) {
|
||||||
|
notesField.classList.remove('collapsed');
|
||||||
|
toggleBtn.title = translate('modals.model.notes.showLess', {}, 'Show less');
|
||||||
|
toggleIcon.className = 'fas fa-chevron-up';
|
||||||
|
notesField.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
} else {
|
||||||
|
notesField.classList.add('collapsed');
|
||||||
|
toggleBtn.title = translate('modals.model.notes.showMore', {}, 'Show more');
|
||||||
|
toggleIcon.className = 'fas fa-chevron-down';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setupLoraSpecificFields(filePath) {
|
function setupLoraSpecificFields(filePath) {
|
||||||
const presetSelector = document.getElementById('preset-selector');
|
const presetSelector = document.getElementById('preset-selector');
|
||||||
const presetValue = document.getElementById('preset-value');
|
const presetValue = document.getElementById('preset-value');
|
||||||
@@ -1125,9 +1192,10 @@ async function handleSendToWorkflow(target, modelType) {
|
|||||||
missingTargetMessage,
|
missingTargetMessage,
|
||||||
});
|
});
|
||||||
} else if (modelType === 'embeddings') {
|
} else if (modelType === 'embeddings') {
|
||||||
// For Embedding: Send as LoRA syntax (embedding name only)
|
const folder = modalElement?.dataset?.folder || '';
|
||||||
const embeddingSyntax = `<embed:${currentFileName}:1>`;
|
const name = currentFileName.replace(/\.[^.]+$/, '');
|
||||||
await sendLoraToWorkflow(embeddingSyntax, false, 'embedding');
|
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
|
await sendEmbeddingToWorkflow(embeddingCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, sendLoraToWorkflow, sendEmbeddingToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js';
|
||||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||||
@@ -47,7 +47,7 @@ export class BulkManager {
|
|||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
addTags: true,
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: true,
|
||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
checkUpdates: true,
|
checkUpdates: true,
|
||||||
@@ -86,7 +86,8 @@ export class BulkManager {
|
|||||||
skipMetadataRefresh: false,
|
skipMetadataRefresh: false,
|
||||||
setFavorite: true,
|
setFavorite: true,
|
||||||
unfavorite: true,
|
unfavorite: true,
|
||||||
repairMetadata: true
|
repairMetadata: true,
|
||||||
|
reimportMetadata: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,13 +504,17 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendAllModelsToWorkflow(replaceMode = false) {
|
async sendAllModelsToWorkflow(replaceMode = false) {
|
||||||
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.currentPageType === MODEL_TYPES.EMBEDDING) {
|
||||||
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
return this._sendAllEmbeddingsToWorkflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
||||||
|
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,6 +546,28 @@ export class BulkManager {
|
|||||||
await sendLoraToWorkflow(loraSyntaxes.join(', '), replaceMode, 'lora');
|
await sendLoraToWorkflow(loraSyntaxes.join(', '), replaceMode, 'lora');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _sendAllEmbeddingsToWorkflow() {
|
||||||
|
const embeddingCodes = [];
|
||||||
|
for (const filepath of state.selectedModels) {
|
||||||
|
const escapedPath = CSS.escape(filepath);
|
||||||
|
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||||
|
if (card) {
|
||||||
|
const folder = card.dataset.folder || '';
|
||||||
|
const name = card.dataset.file_name || '';
|
||||||
|
const code = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
|
embeddingCodes.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embeddingCodes.length === 0) {
|
||||||
|
showToast('No valid embedding data found', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinedCode = embeddingCodes.join(', ');
|
||||||
|
await sendEmbeddingToWorkflow(joinedCode);
|
||||||
|
}
|
||||||
|
|
||||||
showBulkDeleteModal() {
|
showBulkDeleteModal() {
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
@@ -657,6 +684,86 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reimportSelectedRecipes() {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.currentPageType !== 'recipes') {
|
||||||
|
showToast('This operation is only available for recipes', {}, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
const total = filePaths.length;
|
||||||
|
let completed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
const recipeMap = new Map();
|
||||||
|
if (state.virtualScroller?.items) {
|
||||||
|
for (const item of state.virtualScroller.items) {
|
||||||
|
if (item.file_path && item.id) {
|
||||||
|
recipeMap.set(item.file_path, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressUI = state.loadingManager.showEnhancedProgress(
|
||||||
|
`Re-importing recipe 1/${total}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < filePaths.length; i++) {
|
||||||
|
const filePath = filePaths[i];
|
||||||
|
const recipeItem = recipeMap.get(filePath);
|
||||||
|
const recipeId = recipeItem?.id;
|
||||||
|
const recipeName = recipeItem?.title || recipeId || 'Unknown';
|
||||||
|
|
||||||
|
progressUI.updateProgress(
|
||||||
|
Math.floor((i / total) * 100),
|
||||||
|
recipeName,
|
||||||
|
`Re-importing recipe ${Math.min(i + 1, total)}/${total}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!recipeId) {
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/lm/recipe/${recipeId}/reimport`,
|
||||||
|
{ method: 'POST' }
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
completed++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (completed > 0) {
|
||||||
|
await progressUI.complete(
|
||||||
|
`Re-import complete: ${completed} re-imported, ${failed} failed`
|
||||||
|
);
|
||||||
|
const { resetAndReload: recipeResetAndReload } = await import('../api/recipeApi.js');
|
||||||
|
recipeResetAndReload(false, { preserveScroll: false });
|
||||||
|
this.clearSelection();
|
||||||
|
} else {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
showToast('toast.recipes.reimportBulkFailed', {}, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[reimportSelectedRecipes] outer catch:', error);
|
||||||
|
state.loadingManager.hide();
|
||||||
|
showToast('toast.recipes.reimportBulkFailed', {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async repairSelectedRecipes() {
|
async repairSelectedRecipes() {
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
|
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ export class DownloadManager {
|
|||||||
this.apiClient = null;
|
this.apiClient = null;
|
||||||
this.useDefaultPath = false;
|
this.useDefaultPath = false;
|
||||||
|
|
||||||
|
// Batch mode state
|
||||||
|
this.batchModels = [];
|
||||||
|
this.isBatchMode = false;
|
||||||
|
this.editingBatchIndex = -1;
|
||||||
|
|
||||||
this.loadingManager = new LoadingManager();
|
this.loadingManager = new LoadingManager();
|
||||||
this.folderTreeManager = new FolderTreeManager();
|
this.folderTreeManager = new FolderTreeManager();
|
||||||
this.folderClickHandler = null;
|
this.folderClickHandler = null;
|
||||||
@@ -37,6 +42,8 @@ export class DownloadManager {
|
|||||||
this.handleConfirmFileSelection = this.confirmFileSelection.bind(this);
|
this.handleConfirmFileSelection = this.confirmFileSelection.bind(this);
|
||||||
this.handleCloseModal = this.closeModal.bind(this);
|
this.handleCloseModal = this.closeModal.bind(this);
|
||||||
this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this);
|
this.handleToggleDefaultPath = this.toggleDefaultPath.bind(this);
|
||||||
|
this.handleBackToUrlFromBatch = this.backToUrlFromBatch.bind(this);
|
||||||
|
this.handleNextFromBatch = this.nextFromBatch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
showDownloadModal() {
|
showDownloadModal() {
|
||||||
@@ -86,6 +93,10 @@ export class DownloadManager {
|
|||||||
document.getElementById('backToVersionFromFilesBtn').addEventListener('click', this.handleBackToVersionFromFiles);
|
document.getElementById('backToVersionFromFilesBtn').addEventListener('click', this.handleBackToVersionFromFiles);
|
||||||
document.getElementById('confirmFileSelection').addEventListener('click', this.handleConfirmFileSelection);
|
document.getElementById('confirmFileSelection').addEventListener('click', this.handleConfirmFileSelection);
|
||||||
|
|
||||||
|
// Batch preview buttons
|
||||||
|
document.getElementById('backToUrlFromBatchBtn').addEventListener('click', this.handleBackToUrlFromBatch);
|
||||||
|
document.getElementById('nextFromBatchBtn').addEventListener('click', this.handleNextFromBatch);
|
||||||
|
|
||||||
// Default path toggle handler
|
// Default path toggle handler
|
||||||
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath);
|
||||||
}
|
}
|
||||||
@@ -138,6 +149,9 @@ export class DownloadManager {
|
|||||||
this.selectedFile = null;
|
this.selectedFile = null;
|
||||||
|
|
||||||
this.selectedFolder = '';
|
this.selectedFolder = '';
|
||||||
|
this.batchModels = [];
|
||||||
|
this.isBatchMode = false;
|
||||||
|
this.editingBatchIndex = -1;
|
||||||
|
|
||||||
// Clear folder tree selection
|
// Clear folder tree selection
|
||||||
if (this.folderTreeManager) {
|
if (this.folderTreeManager) {
|
||||||
@@ -157,20 +171,27 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validateAndFetchVersions() {
|
async validateAndFetchVersions() {
|
||||||
const url = document.getElementById('modelUrl').value.trim();
|
const rawText = document.getElementById('modelUrl').value.trim();
|
||||||
const errorElement = document.getElementById('urlError');
|
const errorElement = document.getElementById('urlError');
|
||||||
|
const urls = rawText.split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
if (urls.length === 0) {
|
||||||
|
errorElement.textContent = translate('modals.download.errors.invalidUrl');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urls.length === 1) {
|
||||||
|
this.isBatchMode = false;
|
||||||
try {
|
try {
|
||||||
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
|
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
|
||||||
|
|
||||||
this.modelId = this.extractModelId(url);
|
this.modelId = this.extractModelId(urls[0]);
|
||||||
if (!this.modelId) {
|
if (!this.modelId) {
|
||||||
throw new Error(translate('modals.download.errors.invalidUrl'));
|
throw new Error(translate('modals.download.errors.invalidUrl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.retrieveVersionsForModel(this.modelId, this.source);
|
await this.retrieveVersionsForModel(this.modelId, this.source);
|
||||||
|
|
||||||
// If we have a version ID from URL, pre-select it
|
|
||||||
if (this.modelVersionId) {
|
if (this.modelVersionId) {
|
||||||
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
|
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
|
||||||
}
|
}
|
||||||
@@ -181,6 +202,73 @@ export class DownloadManager {
|
|||||||
} finally {
|
} finally {
|
||||||
this.loadingManager.hide();
|
this.loadingManager.hide();
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-URL batch mode
|
||||||
|
this.isBatchMode = true;
|
||||||
|
this.batchModels = [];
|
||||||
|
errorElement.textContent = '';
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const parsed = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
const result = DownloadManager.parseModelUrl(url);
|
||||||
|
if (!result.modelId) {
|
||||||
|
parsed.push({ url, error: translate('modals.download.errors.invalidUrl') });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Dedup by modelId + modelVersionId combo so users can download
|
||||||
|
// different versions of the same model (e.g. latest + a specific version)
|
||||||
|
const dedupKey = result.modelVersionId
|
||||||
|
? `${result.modelId}:${result.modelVersionId}`
|
||||||
|
: result.modelId;
|
||||||
|
if (seen.has(dedupKey)) continue;
|
||||||
|
seen.add(dedupKey);
|
||||||
|
parsed.push({ url, ...result, error: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.length === 0) {
|
||||||
|
errorElement.textContent = translate('modals.download.errors.invalidUrl');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
|
||||||
|
|
||||||
|
let fetched = 0;
|
||||||
|
const total = parsed.filter(p => !p.error).length;
|
||||||
|
|
||||||
|
this.batchModels = new Array(parsed.length);
|
||||||
|
|
||||||
|
const fetchPromises = parsed.map(async (item, index) => {
|
||||||
|
if (item.error) {
|
||||||
|
this.batchModels[index] = { ...item, versions: [], selectedVersion: null };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const versions = await this.apiClient.fetchCivitaiVersions(item.modelId, item.source);
|
||||||
|
fetched++;
|
||||||
|
this.loadingManager.setStatus(`${fetched}/${total}`);
|
||||||
|
|
||||||
|
let selectedVersion = null;
|
||||||
|
if (versions && versions.length > 0) {
|
||||||
|
if (item.modelVersionId) {
|
||||||
|
selectedVersion = versions.find(v => v.id.toString() === item.modelVersionId) || versions[0];
|
||||||
|
} else {
|
||||||
|
selectedVersion = versions[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.batchModels[index] = { ...item, versions: versions || [], selectedVersion };
|
||||||
|
} catch (err) {
|
||||||
|
this.batchModels[index] = { ...item, versions: [], selectedVersion: null, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(fetchPromises);
|
||||||
|
this.loadingManager.hide();
|
||||||
|
|
||||||
|
this.showBatchPreviewStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchVersionsForCurrentModel() {
|
async fetchVersionsForCurrentModel() {
|
||||||
@@ -204,25 +292,30 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extractModelId(url) {
|
static parseModelUrl(url) {
|
||||||
const civarchiveMatch = url.match(/https?:\/\/(?:www\.)?(?:civitaiarchive|civarchive)\.com\/models\/(\d+)/i);
|
const civarchiveMatch = url.match(/https?:\/\/(?:www\.)?(?:civitaiarchive|civarchive)\.com\/models\/(\d+)/i);
|
||||||
if (civarchiveMatch) {
|
if (civarchiveMatch) {
|
||||||
const versionMatch = url.match(/modelVersionId=(\d+)/i);
|
const versionMatch = url.match(/modelVersionId=(\d+)/i);
|
||||||
this.modelVersionId = versionMatch ? versionMatch[1] : null;
|
return {
|
||||||
this.source = 'civarchive';
|
modelId: civarchiveMatch[1],
|
||||||
return civarchiveMatch[1];
|
modelVersionId: versionMatch ? versionMatch[1] : null,
|
||||||
|
source: 'civarchive',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { modelId, modelVersionId } = extractCivitaiModelUrlParts(url);
|
const { modelId, modelVersionId } = extractCivitaiModelUrlParts(url);
|
||||||
if (modelId) {
|
if (modelId) {
|
||||||
this.modelVersionId = modelVersionId;
|
return { modelId, modelVersionId, source: null };
|
||||||
this.source = null;
|
|
||||||
return modelId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modelVersionId = null;
|
return { modelId: null, modelVersionId: null, source: null };
|
||||||
this.source = null;
|
}
|
||||||
return null;
|
|
||||||
|
extractModelId(url) {
|
||||||
|
const result = DownloadManager.parseModelUrl(url);
|
||||||
|
this.modelVersionId = result.modelVersionId;
|
||||||
|
this.source = result.source;
|
||||||
|
return result.modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async openForModelVersion(modelType, modelId, versionId = null) {
|
async openForModelVersion(modelType, modelId, versionId = null) {
|
||||||
@@ -250,7 +343,10 @@ export class DownloadManager {
|
|||||||
document.getElementById('versionStep').style.display = 'block';
|
document.getElementById('versionStep').style.display = 'block';
|
||||||
|
|
||||||
const versionList = document.getElementById('versionList');
|
const versionList = document.getElementById('versionList');
|
||||||
versionList.innerHTML = this.versions.map(version => {
|
const newList = versionList.cloneNode(false);
|
||||||
|
versionList.parentNode.replaceChild(newList, versionList);
|
||||||
|
|
||||||
|
newList.innerHTML = this.versions.map(version => {
|
||||||
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
|
const firstImage = version.images?.find(img => !img.url.endsWith('.mp4'));
|
||||||
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
||||||
|
|
||||||
@@ -326,7 +422,7 @@ export class DownloadManager {
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Add click handlers for version selection and file badge
|
// Add click handlers for version selection and file badge
|
||||||
versionList.addEventListener('click', (event) => {
|
newList.addEventListener('click', (event) => {
|
||||||
const badge = event.target.closest('.file-select-badge');
|
const badge = event.target.closest('.file-select-badge');
|
||||||
if (badge) {
|
if (badge) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -452,18 +548,30 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async proceedToLocation() {
|
async proceedToLocation() {
|
||||||
|
// If editing a batch item's version, save and return to batch preview
|
||||||
|
if (this.isBatchMode && this.editingBatchIndex >= 0) {
|
||||||
|
if (this.currentVersion) {
|
||||||
|
this.batchModels[this.editingBatchIndex].selectedVersion = this.currentVersion;
|
||||||
|
}
|
||||||
|
this.editingBatchIndex = -1;
|
||||||
|
document.getElementById('versionStep').style.display = 'none';
|
||||||
|
this.showBatchPreviewStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In single-URL mode, validate version selection
|
||||||
|
if (!this.isBatchMode) {
|
||||||
if (!this.currentVersion) {
|
if (!this.currentVersion) {
|
||||||
showToast('toast.loras.pleaseSelectVersion', {}, 'error');
|
showToast('toast.loras.pleaseSelectVersion', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.currentVersion.existsLocally) {
|
||||||
const existsLocally = this.currentVersion.existsLocally;
|
|
||||||
if (existsLocally) {
|
|
||||||
showToast('toast.loras.versionExists', {}, 'info');
|
showToast('toast.loras.versionExists', {}, 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('versionStep').style.display = 'none';
|
document.querySelectorAll('.download-step').forEach(step => step.style.display = 'none');
|
||||||
document.getElementById('locationStep').style.display = 'block';
|
document.getElementById('locationStep').style.display = 'block';
|
||||||
await this.proceedToLocationContent();
|
await this.proceedToLocationContent();
|
||||||
}
|
}
|
||||||
@@ -700,15 +808,124 @@ export class DownloadManager {
|
|||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showBatchPreviewStep() {
|
||||||
|
document.querySelectorAll('.download-step').forEach(step => step.style.display = 'none');
|
||||||
|
document.getElementById('batchPreviewStep').style.display = 'block';
|
||||||
|
|
||||||
|
const validCount = this.batchModels.filter(m => !m.error && m.selectedVersion).length;
|
||||||
|
document.getElementById('downloadModalTitle').textContent =
|
||||||
|
translate('modals.download.titleWithType', { type: this.apiClient.apiConfig.config.displayName }) +
|
||||||
|
` (${validCount})`;
|
||||||
|
|
||||||
|
const list = document.getElementById('batchPreviewList');
|
||||||
|
list.innerHTML = this.batchModels.map((item, index) => {
|
||||||
|
if (item.error) {
|
||||||
|
return `
|
||||||
|
<div class="batch-preview-item batch-preview-error" data-index="${index}">
|
||||||
|
<div class="batch-preview-icon">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="batch-preview-info">
|
||||||
|
<div class="batch-preview-name">${item.url}</div>
|
||||||
|
<div class="batch-preview-meta batch-preview-error-text">${item.error}</div>
|
||||||
|
</div>
|
||||||
|
<button class="batch-preview-remove" data-index="${index}" title="${translate('common.actions.remove', {}, 'Remove')}">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ver = item.selectedVersion;
|
||||||
|
const firstImage = ver?.images?.find(img => !img.url.endsWith('.mp4'));
|
||||||
|
const thumbnailUrl = firstImage ? firstImage.url : '/loras_static/images/no-preview.png';
|
||||||
|
const fileSize = ver?.modelSizeKB
|
||||||
|
? (ver.modelSizeKB / 1024).toFixed(1)
|
||||||
|
: (ver?.files?.[0]?.sizeKB ? (ver.files[0].sizeKB / 1024).toFixed(1) : '?');
|
||||||
|
const existsLocally = ver?.existsLocally;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="batch-preview-item ${existsLocally ? 'batch-preview-local' : ''}" data-index="${index}">
|
||||||
|
<div class="batch-preview-thumbnail">
|
||||||
|
<img src="${thumbnailUrl}" alt="">
|
||||||
|
</div>
|
||||||
|
<div class="batch-preview-info">
|
||||||
|
<div class="batch-preview-name">${ver?.name || `Model #${item.modelId}`}</div>
|
||||||
|
<div class="batch-preview-meta">
|
||||||
|
${ver?.baseModel ? `<span>${ver.baseModel}</span>` : ''}
|
||||||
|
<span>${fileSize} MB</span>
|
||||||
|
${existsLocally ? `<span class="batch-preview-local-badge"><i class="fas fa-check"></i> ${translate('modals.download.inLibrary')}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${item.versions.length > 1 ? `
|
||||||
|
<button class="batch-preview-change-version secondary-btn" data-index="${index}">
|
||||||
|
${translate('common.actions.change', {}, 'Change')}
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
list.onclick = (e) => {
|
||||||
|
const removeBtn = e.target.closest('.batch-preview-remove');
|
||||||
|
if (removeBtn) {
|
||||||
|
const idx = parseInt(removeBtn.dataset.index);
|
||||||
|
this.batchModels.splice(idx, 1);
|
||||||
|
this.showBatchPreviewStep();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const changeBtn = e.target.closest('.batch-preview-change-version');
|
||||||
|
if (changeBtn) {
|
||||||
|
const idx = parseInt(changeBtn.dataset.index);
|
||||||
|
this.openBatchVersionEditor(idx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextBtn = document.getElementById('nextFromBatchBtn');
|
||||||
|
nextBtn.disabled = validCount === 0;
|
||||||
|
nextBtn.classList.toggle('disabled', validCount === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
openBatchVersionEditor(index) {
|
||||||
|
this.editingBatchIndex = index;
|
||||||
|
const item = this.batchModels[index];
|
||||||
|
|
||||||
|
this.versions = item.versions;
|
||||||
|
this.currentVersion = item.selectedVersion;
|
||||||
|
|
||||||
|
document.getElementById('batchPreviewStep').style.display = 'none';
|
||||||
|
this.showVersionStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
backToUrlFromBatch() {
|
||||||
|
document.getElementById('batchPreviewStep').style.display = 'none';
|
||||||
|
document.getElementById('urlStep').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFromBatch() {
|
||||||
|
const validModels = this.batchModels.filter(m => !m.error && m.selectedVersion);
|
||||||
|
if (validModels.length === 0) return;
|
||||||
|
this.proceedToLocation();
|
||||||
|
}
|
||||||
|
|
||||||
backToUrl() {
|
backToUrl() {
|
||||||
document.getElementById('versionStep').style.display = 'none';
|
document.getElementById('versionStep').style.display = 'none';
|
||||||
|
if (this.isBatchMode && this.editingBatchIndex >= 0) {
|
||||||
|
this.editingBatchIndex = -1;
|
||||||
|
this.showBatchPreviewStep();
|
||||||
|
} else {
|
||||||
document.getElementById('urlStep').style.display = 'block';
|
document.getElementById('urlStep').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backToVersions() {
|
backToVersions() {
|
||||||
document.getElementById('locationStep').style.display = 'none';
|
document.getElementById('locationStep').style.display = 'none';
|
||||||
|
if (this.isBatchMode) {
|
||||||
|
document.getElementById('batchPreviewStep').style.display = 'block';
|
||||||
|
} else {
|
||||||
document.getElementById('versionStep').style.display = 'block';
|
document.getElementById('versionStep').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
// Clean up folder tree manager
|
// Clean up folder tree manager
|
||||||
@@ -727,16 +944,15 @@ export class DownloadManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine target folder and use_default_paths parameter
|
|
||||||
let targetFolder = '';
|
let targetFolder = '';
|
||||||
let useDefaultPaths = false;
|
let useDefaultPaths = false;
|
||||||
|
|
||||||
if (this.useDefaultPath) {
|
if (this.useDefaultPath) {
|
||||||
useDefaultPaths = true;
|
useDefaultPaths = true;
|
||||||
targetFolder = ''; // Not needed when using default paths
|
|
||||||
} else {
|
} else {
|
||||||
targetFolder = this.folderTreeManager.getSelectedPath();
|
targetFolder = this.folderTreeManager.getSelectedPath();
|
||||||
}
|
}
|
||||||
|
if (!this.isBatchMode) {
|
||||||
const fileParams = this.selectedFile ? {
|
const fileParams = this.selectedFile ? {
|
||||||
type: 'Model',
|
type: 'Model',
|
||||||
format: this.selectedFile.metadata?.format || 'SafeTensor',
|
format: this.selectedFile.metadata?.format || 'SafeTensor',
|
||||||
@@ -757,6 +973,93 @@ export class DownloadManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch download mode
|
||||||
|
const downloadItems = this.batchModels.filter(m => !m.error && m.selectedVersion && !m.selectedVersion.existsLocally);
|
||||||
|
if (downloadItems.length === 0) {
|
||||||
|
showToast('toast.loras.downloadCompleted', {}, 'info');
|
||||||
|
modalManager.closeModal('downloadModal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalManager.closeModal('downloadModal');
|
||||||
|
|
||||||
|
const batchDownloadId = Date.now().toString();
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${batchDownloadId}`);
|
||||||
|
|
||||||
|
const loadingManager = state.loadingManager || this.loadingManager;
|
||||||
|
const updateProgress = loadingManager.showDownloadProgress(downloadItems.length);
|
||||||
|
|
||||||
|
let completedDownloads = 0;
|
||||||
|
let failedDownloads = 0;
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'download_id') return;
|
||||||
|
|
||||||
|
if (data.status === 'progress' && data.download_id?.startsWith(batchDownloadId)) {
|
||||||
|
const current = downloadItems[completedDownloads + failedDownloads];
|
||||||
|
const name = current?.selectedVersion?.name || `#${completedDownloads + failedDownloads + 1}`;
|
||||||
|
const metrics = {
|
||||||
|
bytesDownloaded: data.bytes_downloaded,
|
||||||
|
totalBytes: data.total_bytes,
|
||||||
|
bytesPerSecond: data.bytes_per_second,
|
||||||
|
};
|
||||||
|
updateProgress(data.progress, completedDownloads, name, metrics);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
ws.onopen = resolve;
|
||||||
|
ws.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < downloadItems.length; i++) {
|
||||||
|
const item = downloadItems[i];
|
||||||
|
const ver = item.selectedVersion;
|
||||||
|
const name = ver?.name || `Model #${item.modelId}`;
|
||||||
|
|
||||||
|
updateProgress(0, completedDownloads, name);
|
||||||
|
loadingManager.setStatus(`${i + 1}/${downloadItems.length}: ${name}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.apiClient.downloadModel(
|
||||||
|
item.modelId,
|
||||||
|
ver.id,
|
||||||
|
modelRoot,
|
||||||
|
targetFolder,
|
||||||
|
useDefaultPaths,
|
||||||
|
batchDownloadId,
|
||||||
|
item.source
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
failedDownloads++;
|
||||||
|
} else {
|
||||||
|
completedDownloads++;
|
||||||
|
updateProgress(100, completedDownloads, '');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to download ${name}:`, err);
|
||||||
|
failedDownloads++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
loadingManager.hide();
|
||||||
|
|
||||||
|
if (failedDownloads === 0) {
|
||||||
|
showToast('toast.loras.allDownloadSuccessful', { count: completedDownloads }, 'success');
|
||||||
|
} else {
|
||||||
|
showToast('toast.loras.downloadPartialSuccess', {
|
||||||
|
completed: completedDownloads,
|
||||||
|
total: downloadItems.length,
|
||||||
|
}, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetAndReload(true);
|
||||||
|
}
|
||||||
|
|
||||||
async downloadVersionWithDefaults(modelType, modelId, versionId, {
|
async downloadVersionWithDefaults(modelType, modelId, versionId, {
|
||||||
versionName = '',
|
versionName = '',
|
||||||
source = null,
|
source = null,
|
||||||
|
|||||||
@@ -866,6 +866,100 @@ async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, mess
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendTextToNodes(nodeIds, nodesMap, text, mode, messages = {}) {
|
||||||
|
const {
|
||||||
|
successMessage = 'Updated workflow node',
|
||||||
|
failureMessage = 'Failed to update workflow node',
|
||||||
|
missingTargetMessage = 'No target node selected',
|
||||||
|
} = messages;
|
||||||
|
|
||||||
|
const targetIds = Array.isArray(nodeIds) ? nodeIds : [];
|
||||||
|
if (targetIds.length === 0) {
|
||||||
|
showToast(missingTargetMessage, {}, 'warning');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const references = targetIds
|
||||||
|
.map((nodeKey) => resolveNodeReference(nodeKey, nodesMap))
|
||||||
|
.filter((reference) => reference && reference.node_id !== undefined);
|
||||||
|
|
||||||
|
if (references.length === 0) {
|
||||||
|
showToast(missingTargetMessage, {}, 'warning');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lm/update-node-widget', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
widget_name: 'text',
|
||||||
|
value: text,
|
||||||
|
mode: mode || 'append',
|
||||||
|
node_ids: references,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
showToast(successMessage, {}, 'success');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = result?.error || failureMessage;
|
||||||
|
showToast(errorMessage, {}, 'error');
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send text to workflow:', error);
|
||||||
|
showToast(failureMessage, {}, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmbeddingToWorkflow(embeddingCode) {
|
||||||
|
const registry = await fetchWorkflowRegistry();
|
||||||
|
if (!registry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textNodes = filterRegistryNodes(registry.nodes, (node) => {
|
||||||
|
if (!isNodeEnabled(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return node.capabilities?.has_text_widget === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeKeys = Object.keys(textNodes);
|
||||||
|
if (nodeKeys.length === 0) {
|
||||||
|
showToast('uiHelpers.workflow.noMatchingNodes', {}, 'warning');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
successMessage: translate('uiHelpers.workflow.embeddingAdded', {}, 'Embedding added to workflow'),
|
||||||
|
failureMessage: translate('uiHelpers.workflow.embeddingFailed', {}, 'Failed to add embedding'),
|
||||||
|
missingTargetMessage: translate('uiHelpers.workflow.noTargetNodeSelected', {}, 'No target node selected'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = (selectedNodeIds) =>
|
||||||
|
sendTextToNodes(selectedNodeIds, textNodes, embeddingCode, 'append', messages);
|
||||||
|
|
||||||
|
if (nodeKeys.length === 1) {
|
||||||
|
return await handleSend([nodeKeys[0]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionType = translate('uiHelpers.nodeSelector.embedding', {}, 'Embedding');
|
||||||
|
|
||||||
|
showNodeSelector(textNodes, {
|
||||||
|
actionType,
|
||||||
|
actionMode: '',
|
||||||
|
onSend: handleSend,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Global variable to track active node selector state
|
// Global variable to track active node selector state
|
||||||
let nodeSelectorState = {
|
let nodeSelectorState = {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
@@ -904,7 +998,9 @@ function showNodeSelector(nodes, options = {}) {
|
|||||||
nodeSelectorState.enableSendAll = options.enableSendAll !== false;
|
nodeSelectorState.enableSendAll = options.enableSendAll !== false;
|
||||||
|
|
||||||
// Generate node list HTML with icons and proper colors
|
// Generate node list HTML with icons and proper colors
|
||||||
const nodeItems = Object.entries(safeNodes).map(([nodeKey, node]) => {
|
const nodeItems = Object.entries(safeNodes)
|
||||||
|
.sort(([, a], [, b]) => a.type - b.type || a.id - b.id)
|
||||||
|
.map(([nodeKey, node]) => {
|
||||||
const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
|
const iconClass = NODE_TYPE_ICONS[node.type] || 'fas fa-question-circle';
|
||||||
const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
|
const bgColor = node.bgcolor || DEFAULT_NODE_COLOR;
|
||||||
const graphLabel = node.graph_name ? ` (${node.graph_name})` : '';
|
const graphLabel = node.graph_name ? ` (${node.graph_name})` : '';
|
||||||
|
|||||||
@@ -10,17 +10,27 @@
|
|||||||
{% block additional_components %}
|
{% block additional_components %}
|
||||||
|
|
||||||
<div id="checkpointContextMenu" class="context-menu" style="display: none;">
|
<div id="checkpointContextMenu" class="context-menu" style="display: none;">
|
||||||
|
<!-- Metadata -->
|
||||||
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
||||||
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Workflow -->
|
||||||
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
||||||
<div class="context-menu-item" data-action="sendworkflow"><i class="fas fa-paper-plane"></i> {{ t('checkpoints.contextMenu.sendToWorkflow') }}</div>
|
<div class="context-menu-item" data-action="sendworkflow"><i class="fas fa-paper-plane"></i> {{ t('checkpoints.contextMenu.sendToWorkflow') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Media / Preview -->
|
||||||
<div class="context-menu-item" data-action="preview"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.openExamples') }}</div>
|
<div class="context-menu-item" data-action="preview"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.openExamples') }}</div>
|
||||||
<div class="context-menu-item" data-action="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadExamples') }}</div>
|
<div class="context-menu-item" data-action="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadExamples') }}</div>
|
||||||
<div class="context-menu-item" data-action="replace-preview"><i class="fas fa-image"></i> {{ t('loras.contextMenu.replacePreview') }}</div>
|
<div class="context-menu-item" data-action="replace-preview"><i class="fas fa-image"></i> {{ t('loras.contextMenu.replacePreview') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Attributes -->
|
||||||
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}</div>
|
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Organization -->
|
||||||
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.moveToFolder') }}</div>
|
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.moveToFolder') }}</div>
|
||||||
<div class="context-menu-item" data-action="move-other"><i class="fas fa-exchange-alt"></i> {{ t('checkpoints.contextMenu.moveToOtherTypeFolder', {otherType: '...'}) }}</div>
|
<div class="context-menu-item" data-action="move-other"><i class="fas fa-exchange-alt"></i> {{ t('checkpoints.contextMenu.moveToOtherTypeFolder', {otherType: '...'}) }}</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<!-- Destructive -->
|
||||||
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> {{ t('loras.contextMenu.excludeModel') }}</div>
|
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> {{ t('loras.contextMenu.excludeModel') }}</div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{ t('loras.contextMenu.deleteModel') }}</div>
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{ t('loras.contextMenu.deleteModel') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- <div class="context-menu-item" data-action="civitai">
|
<!-- <div class="context-menu-item" data-action="civitai">
|
||||||
<i class="fas fa-external-link-alt"></i> View on Civitai
|
<i class="fas fa-external-link-alt"></i> View on Civitai
|
||||||
</div> -->
|
</div> -->
|
||||||
|
<!-- Metadata -->
|
||||||
<div class="context-menu-item" data-action="refresh-metadata">
|
<div class="context-menu-item" data-action="refresh-metadata">
|
||||||
<i class="fas fa-sync"></i> <span>{{ t('loras.contextMenu.refreshMetadata') }}</span>
|
<i class="fas fa-sync"></i> <span>{{ t('loras.contextMenu.refreshMetadata') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
<div class="context-menu-item" data-action="relink-civitai">
|
<div class="context-menu-item" data-action="relink-civitai">
|
||||||
<i class="fas fa-link"></i> <span>{{ t('loras.contextMenu.relinkCivitai') }}</span>
|
<i class="fas fa-link"></i> <span>{{ t('loras.contextMenu.relinkCivitai') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Workflow -->
|
||||||
<div class="context-menu-item" data-action="copyname">
|
<div class="context-menu-item" data-action="copyname">
|
||||||
<i class="fas fa-copy"></i> <span>{{ t('loras.contextMenu.copySyntax') }}</span>
|
<i class="fas fa-copy"></i> <span>{{ t('loras.contextMenu.copySyntax') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,6 +26,8 @@
|
|||||||
<div class="context-menu-item" data-action="sendreplace">
|
<div class="context-menu-item" data-action="sendreplace">
|
||||||
<i class="fas fa-exchange-alt"></i> <span>{{ t('loras.contextMenu.sendToWorkflowReplace') }}</span>
|
<i class="fas fa-exchange-alt"></i> <span>{{ t('loras.contextMenu.sendToWorkflowReplace') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Media / Preview -->
|
||||||
<div class="context-menu-item" data-action="preview">
|
<div class="context-menu-item" data-action="preview">
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.openExamples') }}</span>
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.openExamples') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,13 +37,18 @@
|
|||||||
<div class="context-menu-item" data-action="replace-preview">
|
<div class="context-menu-item" data-action="replace-preview">
|
||||||
<i class="fas fa-image"></i> <span>{{ t('loras.contextMenu.replacePreview') }}</span>
|
<i class="fas fa-image"></i> <span>{{ t('loras.contextMenu.replacePreview') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Attributes -->
|
||||||
<div class="context-menu-item" data-action="set-nsfw">
|
<div class="context-menu-item" data-action="set-nsfw">
|
||||||
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.contextMenu.setContentRating') }}</span>
|
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.contextMenu.setContentRating') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Organization -->
|
||||||
<div class="context-menu-item" data-action="move">
|
<div class="context-menu-item" data-action="move">
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.moveToFolder') }}</span>
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.contextMenu.moveToFolder') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<!-- Destructive -->
|
||||||
<div class="context-menu-item" data-action="exclude">
|
<div class="context-menu-item" data-action="exclude">
|
||||||
<i class="fas fa-eye-slash"></i> <span>{{ t('loras.contextMenu.excludeModel') }}</span>
|
<i class="fas fa-eye-slash"></i> <span>{{ t('loras.contextMenu.excludeModel') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +63,27 @@
|
|||||||
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-section" data-section="metadata">
|
||||||
|
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.metadata') }}</div>
|
||||||
|
<div class="context-menu-item" data-action="refresh-all">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="check-updates">
|
||||||
|
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="repair-metadata">
|
||||||
|
<i class="fas fa-tools"></i> <span>{{ t('loras.bulkOperations.repairMetadata') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="reimport-metadata">
|
||||||
|
<i class="fas fa-undo-alt"></i> <span>{{ t('loras.bulkOperations.reimportMetadata') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="skip-metadata-refresh">
|
||||||
|
<i class="fas fa-ban"></i> <span>{{ t('loras.bulkOperations.skipMetadataRefresh') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="resume-metadata-refresh">
|
||||||
|
<i class="fas fa-redo"></i> <span>{{ t('loras.bulkOperations.resumeMetadataRefresh') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="context-menu-section" data-section="workflow">
|
<div class="context-menu-section" data-section="workflow">
|
||||||
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.workflow') }}</div>
|
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.workflow') }}</div>
|
||||||
<div class="context-menu-item has-submenu" data-has-submenu="send-to-workflow">
|
<div class="context-menu-item has-submenu" data-has-submenu="send-to-workflow">
|
||||||
@@ -72,24 +103,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-section" data-section="metadata">
|
|
||||||
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.metadata') }}</div>
|
|
||||||
<div class="context-menu-item" data-action="refresh-all">
|
|
||||||
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-item" data-action="check-updates">
|
|
||||||
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-item" data-action="repair-metadata">
|
|
||||||
<i class="fas fa-tools"></i> <span>{{ t('loras.bulkOperations.repairMetadata') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-item" data-action="skip-metadata-refresh">
|
|
||||||
<i class="fas fa-ban"></i> <span>{{ t('loras.bulkOperations.skipMetadataRefresh') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-item" data-action="resume-metadata-refresh">
|
|
||||||
<i class="fas fa-redo"></i> <span>{{ t('loras.bulkOperations.resumeMetadataRefresh') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-section" data-section="attributes">
|
<div class="context-menu-section" data-section="attributes">
|
||||||
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.attributes') }}</div>
|
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.attributes') }}</div>
|
||||||
<div class="context-menu-item" data-action="add-tags">
|
<div class="context-menu-item" data-action="add-tags">
|
||||||
@@ -105,15 +118,6 @@
|
|||||||
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
|
<i class="fas fa-exclamation-triangle"></i> <span>{{ t('loras.bulkOperations.setContentRating') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-section" data-section="organize">
|
|
||||||
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.organize') }}</div>
|
|
||||||
<div class="context-menu-item" data-action="auto-organize">
|
|
||||||
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-item" data-action="move-all">
|
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="context-menu-section" data-section="download">
|
<div class="context-menu-section" data-section="download">
|
||||||
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.download') }}</div>
|
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.download') }}</div>
|
||||||
<div class="context-menu-item" data-action="download-example-images">
|
<div class="context-menu-item" data-action="download-example-images">
|
||||||
@@ -123,6 +127,15 @@
|
|||||||
<i class="fas fa-download"></i> <span>{{ t('loras.bulkOperations.downloadMissingLoras') }}</span>
|
<i class="fas fa-download"></i> <span>{{ t('loras.bulkOperations.downloadMissingLoras') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-section" data-section="organize">
|
||||||
|
<div class="context-menu-section-header">{{ t('loras.bulkOperations.sections.organize') }}</div>
|
||||||
|
<div class="context-menu-item" data-action="auto-organize">
|
||||||
|
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="move-all">
|
||||||
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete-all">
|
<div class="context-menu-item delete-item" data-action="delete-all">
|
||||||
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
||||||
|
|||||||
@@ -18,6 +18,20 @@
|
|||||||
<button class="sidebar-action-btn" id="sidebarPinToggle" title="{{ t('sidebar.unpinSidebar') }}">
|
<button class="sidebar-action-btn" id="sidebarPinToggle" title="{{ t('sidebar.unpinSidebar') }}">
|
||||||
<i class="fas fa-thumbtack"></i>
|
<i class="fas fa-thumbtack"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="sidebar-action-btn" id="sidebarMoreToggle" title="{{ t('sidebar.moreOptions') }}">
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Dropdown menu for more options -->
|
||||||
|
<div class="sidebar-more-dropdown" id="sidebarMoreDropdown">
|
||||||
|
<div class="sidebar-dropdown-item" data-action="toggle-pin">
|
||||||
|
<i class="fas fa-thumbtack"></i>
|
||||||
|
<span id="sidebarMorePinLabel">{{ t('sidebar.pinSidebar') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-dropdown-item" data-action="toggle-hide">
|
||||||
|
<i class="fas fa-eye-slash"></i>
|
||||||
|
<span>{{ t('sidebar.hideOnThisPage') }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
|
|||||||
@@ -9,16 +9,31 @@
|
|||||||
<!-- Step 1: URL Input -->
|
<!-- Step 1: URL Input -->
|
||||||
<div class="download-step" id="urlStep">
|
<div class="download-step" id="urlStep">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="modelUrl" id="modelUrlLabel">{{ t('modals.download.url') }}:</label>
|
<label for="modelUrl" id="modelUrlLabel">{{ t('modals.download.civitaiUrl') }}</label>
|
||||||
<input type="text" id="modelUrl" placeholder="{{ t('modals.download.placeholder') }}" />
|
<textarea id="modelUrl" rows="5" placeholder="{{ t('modals.download.placeholder') }}"></textarea>
|
||||||
<div class="error-message" id="urlError"></div>
|
<div class="error-message" id="urlError"></div>
|
||||||
|
<div class="input-hint">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>{{ t('modals.download.urlHint') }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="primary-btn" id="nextFromUrl">{{ t('common.actions.next') }}</button>
|
<button class="primary-btn" id="nextFromUrl">{{ t('common.actions.next') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Version Selection -->
|
<!-- Step 2: Batch Preview (multi-URL mode) -->
|
||||||
|
<div class="download-step" id="batchPreviewStep" style="display: none;">
|
||||||
|
<div class="batch-preview-list" id="batchPreviewList">
|
||||||
|
<!-- Batch items will be inserted here dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="secondary-btn" id="backToUrlFromBatchBtn">{{ t('common.actions.back') }}</button>
|
||||||
|
<button class="primary-btn" id="nextFromBatchBtn">{{ t('common.actions.next') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Version Selection (single-URL or per-item editing) -->
|
||||||
<div class="download-step" id="versionStep" style="display: none;">
|
<div class="download-step" id="versionStep" style="display: none;">
|
||||||
<div class="version-list" id="versionList">
|
<div class="version-list" id="versionList">
|
||||||
<!-- Versions will be inserted here dynamically -->
|
<!-- Versions will be inserted here dynamically -->
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
id="civitaiApiKey"
|
id="civitaiApiKey"
|
||||||
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}"
|
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}"
|
||||||
value="{{ settings.get('civitai_api_key', '') }}"
|
value="{{ settings.get('civitai_api_key', '') }}"
|
||||||
|
autocomplete="new-password"
|
||||||
onblur="settingsManager.saveInputSetting('civitaiApiKey', 'civitai_api_key')"
|
onblur="settingsManager.saveInputSetting('civitaiApiKey', 'civitai_api_key')"
|
||||||
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
||||||
<button class="toggle-visibility">
|
<button class="toggle-visibility">
|
||||||
@@ -371,6 +372,7 @@
|
|||||||
<div class="api-key-input">
|
<div class="api-key-input">
|
||||||
<input type="password" id="proxyPassword"
|
<input type="password" id="proxyPassword"
|
||||||
placeholder="{{ t('settings.proxySettings.proxyPasswordPlaceholder') }}"
|
placeholder="{{ t('settings.proxySettings.proxyPasswordPlaceholder') }}"
|
||||||
|
autocomplete="new-password"
|
||||||
onblur="settingsManager.saveInputSetting('proxyPassword', 'proxy_password')"
|
onblur="settingsManager.saveInputSetting('proxyPassword', 'proxy_password')"
|
||||||
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
|
||||||
<button class="toggle-visibility">
|
<button class="toggle-visibility">
|
||||||
|
|||||||
@@ -10,15 +10,26 @@
|
|||||||
{% block additional_components %}
|
{% block additional_components %}
|
||||||
|
|
||||||
<div id="embeddingContextMenu" class="context-menu" style="display: none;">
|
<div id="embeddingContextMenu" class="context-menu" style="display: none;">
|
||||||
|
<!-- Metadata -->
|
||||||
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
||||||
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Workflow -->
|
||||||
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
||||||
|
<div class="context-menu-item" data-action="sendtoworkflow"><i class="fas fa-paper-plane"></i> {{ t('checkpoints.contextMenu.sendToWorkflow') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Media / Preview -->
|
||||||
<div class="context-menu-item" data-action="preview"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.openExamples') }}</div>
|
<div class="context-menu-item" data-action="preview"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.openExamples') }}</div>
|
||||||
<div class="context-menu-item" data-action="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadExamples') }}</div>
|
<div class="context-menu-item" data-action="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadExamples') }}</div>
|
||||||
<div class="context-menu-item" data-action="replace-preview"><i class="fas fa-image"></i> {{ t('loras.contextMenu.replacePreview') }}</div>
|
<div class="context-menu-item" data-action="replace-preview"><i class="fas fa-image"></i> {{ t('loras.contextMenu.replacePreview') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Attributes -->
|
||||||
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}</div>
|
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Organization -->
|
||||||
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.moveToFolder') }}</div>
|
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.moveToFolder') }}</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<!-- Destructive -->
|
||||||
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> {{ t('loras.contextMenu.excludeModel') }}</div>
|
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> {{ t('loras.contextMenu.excludeModel') }}</div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{ t('loras.contextMenu.deleteModel') }}</div>
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{ t('loras.contextMenu.deleteModel') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,15 @@
|
|||||||
|
|
||||||
<div id="recipeContextMenu" class="context-menu" style="display: none;">
|
<div id="recipeContextMenu" class="context-menu" style="display: none;">
|
||||||
<!-- <div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div> -->
|
<!-- <div class="context-menu-item" data-action="details"><i class="fas fa-info-circle"></i> View Details</div> -->
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="context-menu-item" data-action="repair">
|
||||||
|
<i class="fas fa-tools"></i> {{ t('loras.contextMenu.repairMetadata') }}
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="reimport">
|
||||||
|
<i class="fas fa-undo-alt"></i> {{ t('loras.contextMenu.reimportMetadata') }}
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Workflow / Share -->
|
||||||
<div class="context-menu-item" data-action="share"><i class="fas fa-share-alt"></i> {{
|
<div class="context-menu-item" data-action="share"><i class="fas fa-share-alt"></i> {{
|
||||||
t('loras.contextMenu.shareRecipe') }}</div>
|
t('loras.contextMenu.shareRecipe') }}</div>
|
||||||
<div class="context-menu-item" data-action="copy"><i class="fas fa-copy"></i> {{
|
<div class="context-menu-item" data-action="copy"><i class="fas fa-copy"></i> {{
|
||||||
@@ -25,19 +34,23 @@
|
|||||||
t('loras.contextMenu.sendToWorkflowAppend') }}</div>
|
t('loras.contextMenu.sendToWorkflowAppend') }}</div>
|
||||||
<div class="context-menu-item" data-action="sendreplace"><i class="fas fa-exchange-alt"></i> {{
|
<div class="context-menu-item" data-action="sendreplace"><i class="fas fa-exchange-alt"></i> {{
|
||||||
t('loras.contextMenu.sendToWorkflowReplace') }}</div>
|
t('loras.contextMenu.sendToWorkflowReplace') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Recipe-specific -->
|
||||||
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> {{
|
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> {{
|
||||||
t('loras.contextMenu.viewAllLoras') }}</div>
|
t('loras.contextMenu.viewAllLoras') }}</div>
|
||||||
<div class="context-menu-item download-missing-item" data-action="download-missing"><i class="fas fa-download"></i>
|
<div class="context-menu-item download-missing-item" data-action="download-missing"><i class="fas fa-download"></i>
|
||||||
{{ t('loras.contextMenu.downloadMissingLoras') }}</div>
|
{{ t('loras.contextMenu.downloadMissingLoras') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Attributes -->
|
||||||
<div class="context-menu-item" data-action="set-nsfw">
|
<div class="context-menu-item" data-action="set-nsfw">
|
||||||
<i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}
|
<i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="context-menu-item" data-action="repair">
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
<i class="fas fa-tools"></i> {{ t('loras.contextMenu.repairMetadata') }}
|
<!-- Organization -->
|
||||||
</div>
|
|
||||||
<div class="context-menu-separator"></div>
|
|
||||||
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{
|
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{
|
||||||
t('loras.contextMenu.moveToFolder') }}</div>
|
t('loras.contextMenu.moveToFolder') }}</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<!-- Destructive -->
|
||||||
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{
|
||||||
t('loras.contextMenu.deleteRecipe') }}</div>
|
t('loras.contextMenu.deleteRecipe') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -754,6 +754,7 @@ async def test_update_node_widget_sends_payload():
|
|||||||
"widget_name": "ckpt_name",
|
"widget_name": "ckpt_name",
|
||||||
"value": "models/checkpoints/model.ckpt",
|
"value": "models/checkpoints/model.ckpt",
|
||||||
"graph_id": "root",
|
"graph_id": "root",
|
||||||
|
"mode": "replace",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -27,9 +27,12 @@ async def _finalize_usage_stats(tasks):
|
|||||||
|
|
||||||
def _prepare_usage_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, sleep_override=None):
|
def _prepare_usage_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, sleep_override=None):
|
||||||
UsageStats._instance = None
|
UsageStats._instance = None
|
||||||
stats_root = tmp_path / "loras"
|
settings_dir = tmp_path / "settings"
|
||||||
stats_root.mkdir(parents=True, exist_ok=True)
|
settings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(stats_root)])
|
monkeypatch.setattr(usage_stats_module, "get_settings_dir", lambda create=True: str(settings_dir))
|
||||||
|
loras_root = tmp_path / "loras"
|
||||||
|
loras_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(loras_root)])
|
||||||
|
|
||||||
created_tasks = []
|
created_tasks = []
|
||||||
real_create_task = usage_stats_module.asyncio.create_task
|
real_create_task = usage_stats_module.asyncio.create_task
|
||||||
@@ -45,7 +48,7 @@ def _prepare_usage_stats(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, sle
|
|||||||
monkeypatch.setattr(usage_stats_module.asyncio, "sleep", sleep_override)
|
monkeypatch.setattr(usage_stats_module.asyncio, "sleep", sleep_override)
|
||||||
|
|
||||||
stats = UsageStats()
|
stats = UsageStats()
|
||||||
return stats, created_tasks, stats_root
|
return stats, created_tasks, settings_dir, loras_root
|
||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_converts_legacy_format(tmp_path, monkeypatch):
|
async def test_usage_stats_converts_legacy_format(tmp_path, monkeypatch):
|
||||||
@@ -57,12 +60,15 @@ async def test_usage_stats_converts_legacy_format(tmp_path, monkeypatch):
|
|||||||
}
|
}
|
||||||
|
|
||||||
UsageStats._instance = None
|
UsageStats._instance = None
|
||||||
stats_root = tmp_path / "loras"
|
settings_dir = tmp_path / "settings"
|
||||||
stats_root.mkdir(parents=True, exist_ok=True)
|
settings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(stats_root)])
|
monkeypatch.setattr(usage_stats_module, "get_settings_dir", lambda create=True: str(settings_dir))
|
||||||
|
loras_root = tmp_path / "loras"
|
||||||
|
loras_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(loras_root)])
|
||||||
|
|
||||||
stats_path = stats_root / UsageStats.STATS_FILENAME
|
old_stats_path = loras_root / UsageStats.STATS_FILENAME
|
||||||
stats_path.write_text(json.dumps(legacy_stats), encoding="utf-8")
|
old_stats_path.write_text(json.dumps(legacy_stats), encoding="utf-8")
|
||||||
|
|
||||||
created_tasks = []
|
created_tasks = []
|
||||||
real_create_task = usage_stats_module.asyncio.create_task
|
real_create_task = usage_stats_module.asyncio.create_task
|
||||||
@@ -83,20 +89,23 @@ async def test_usage_stats_converts_legacy_format(tmp_path, monkeypatch):
|
|||||||
assert converted["checkpoints"]["hash1"] == {"total": 3, "history": {today: 3}}
|
assert converted["checkpoints"]["hash1"] == {"total": 3, "history": {today: 3}}
|
||||||
assert converted["loras"]["hash2"] == {"total": 5, "history": {today: 5}}
|
assert converted["loras"]["hash2"] == {"total": 5, "history": {today: 5}}
|
||||||
|
|
||||||
backup_path = stats_path.with_suffix(stats_path.suffix + UsageStats.BACKUP_SUFFIX)
|
new_stats_path = settings_dir / "stats" / UsageStats.STATS_FILENAME
|
||||||
|
assert new_stats_path.exists()
|
||||||
|
|
||||||
|
backup_path = new_stats_path.with_suffix(new_stats_path.suffix + UsageStats.BACKUP_SUFFIX)
|
||||||
assert backup_path.exists()
|
assert backup_path.exists()
|
||||||
|
|
||||||
await _finalize_usage_stats(created_tasks)
|
await _finalize_usage_stats(created_tasks)
|
||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_save_stats_persists_file(tmp_path, monkeypatch):
|
async def test_usage_stats_save_stats_persists_file(tmp_path, monkeypatch):
|
||||||
stats, tasks, stats_root = _prepare_usage_stats(tmp_path, monkeypatch)
|
stats, tasks, settings_dir, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
||||||
stats.stats["total_executions"] = 4
|
stats.stats["total_executions"] = 4
|
||||||
|
|
||||||
saved = await stats.save_stats(force=True)
|
saved = await stats.save_stats(force=True)
|
||||||
assert saved is True
|
assert saved is True
|
||||||
|
|
||||||
stats_path = stats_root / UsageStats.STATS_FILENAME
|
stats_path = settings_dir / "stats" / UsageStats.STATS_FILENAME
|
||||||
persisted = json.loads(stats_path.read_text(encoding="utf-8"))
|
persisted = json.loads(stats_path.read_text(encoding="utf-8"))
|
||||||
assert persisted["total_executions"] == 4
|
assert persisted["total_executions"] == 4
|
||||||
assert persisted["last_save_time"] == stats.stats["last_save_time"]
|
assert persisted["last_save_time"] == stats.stats["last_save_time"]
|
||||||
@@ -110,7 +119,7 @@ async def test_usage_stats_background_processor_handles_pending_prompts(tmp_path
|
|||||||
async def fast_sleep(_seconds):
|
async def fast_sleep(_seconds):
|
||||||
await real_sleep(0.01)
|
await real_sleep(0.01)
|
||||||
|
|
||||||
stats, tasks, _ = _prepare_usage_stats(tmp_path, monkeypatch, sleep_override=fast_sleep)
|
stats, tasks, _, _ = _prepare_usage_stats(tmp_path, monkeypatch, sleep_override=fast_sleep)
|
||||||
|
|
||||||
metadata_calls = []
|
metadata_calls = []
|
||||||
# Use string literals directly to avoid dependency on conditional imports
|
# Use string literals directly to avoid dependency on conditional imports
|
||||||
@@ -155,7 +164,7 @@ async def test_usage_stats_background_processor_handles_pending_prompts(tmp_path
|
|||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_calculates_pending_checkpoint_hash_on_demand(tmp_path, monkeypatch):
|
async def test_usage_stats_calculates_pending_checkpoint_hash_on_demand(tmp_path, monkeypatch):
|
||||||
stats, tasks, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
stats, tasks, _, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
||||||
|
|
||||||
metadata_payload = {
|
metadata_payload = {
|
||||||
"models": {
|
"models": {
|
||||||
@@ -195,7 +204,7 @@ async def test_usage_stats_calculates_pending_checkpoint_hash_on_demand(tmp_path
|
|||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_skips_failed_checkpoint_hash_retry(tmp_path, monkeypatch):
|
async def test_usage_stats_skips_failed_checkpoint_hash_retry(tmp_path, monkeypatch):
|
||||||
stats, tasks, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
stats, tasks, _, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
||||||
|
|
||||||
metadata_payload = {
|
metadata_payload = {
|
||||||
"models": {
|
"models": {
|
||||||
@@ -234,7 +243,7 @@ async def test_usage_stats_skips_failed_checkpoint_hash_retry(tmp_path, monkeypa
|
|||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_resolves_manually_copied_checkpoint_from_disk(tmp_path, monkeypatch):
|
async def test_usage_stats_resolves_manually_copied_checkpoint_from_disk(tmp_path, monkeypatch):
|
||||||
stats, tasks, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
stats, tasks, _, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
||||||
|
|
||||||
checkpoints_root = tmp_path / "checkpoints"
|
checkpoints_root = tmp_path / "checkpoints"
|
||||||
checkpoints_root.mkdir()
|
checkpoints_root.mkdir()
|
||||||
@@ -273,7 +282,7 @@ async def test_usage_stats_resolves_manually_copied_checkpoint_from_disk(tmp_pat
|
|||||||
|
|
||||||
|
|
||||||
async def test_usage_stats_skips_name_fallback_for_missing_lora_hash(tmp_path, monkeypatch):
|
async def test_usage_stats_skips_name_fallback_for_missing_lora_hash(tmp_path, monkeypatch):
|
||||||
stats, tasks, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
stats, tasks, _, _ = _prepare_usage_stats(tmp_path, monkeypatch)
|
||||||
|
|
||||||
metadata_payload = {
|
metadata_payload = {
|
||||||
"models": {},
|
"models": {},
|
||||||
@@ -294,3 +303,79 @@ async def test_usage_stats_skips_name_fallback_for_missing_lora_hash(tmp_path, m
|
|||||||
assert not any(key.startswith("name:") for key in stats.stats["loras"])
|
assert not any(key.startswith("name:") for key in stats.stats["loras"])
|
||||||
|
|
||||||
await _finalize_usage_stats(tasks)
|
await _finalize_usage_stats(tasks)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_usage_stats_migrates_from_old_location(tmp_path, monkeypatch):
|
||||||
|
settings_dir = tmp_path / "settings"
|
||||||
|
settings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module, "get_settings_dir", lambda create=True: str(settings_dir))
|
||||||
|
loras_root = tmp_path / "loras"
|
||||||
|
loras_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(loras_root)])
|
||||||
|
|
||||||
|
old_data = {
|
||||||
|
"checkpoints": {},
|
||||||
|
"loras": {"lora-hash": {"total": 3, "history": {"2025-01-01": 3}}},
|
||||||
|
"embeddings": {},
|
||||||
|
"total_executions": 3,
|
||||||
|
"last_save_time": 100.0,
|
||||||
|
}
|
||||||
|
old_path = loras_root / UsageStats.STATS_FILENAME
|
||||||
|
old_path.write_text(json.dumps(old_data), encoding="utf-8")
|
||||||
|
|
||||||
|
created_tasks = []
|
||||||
|
real_create_task = usage_stats_module.asyncio.create_task
|
||||||
|
|
||||||
|
def _track_task(coro):
|
||||||
|
task = real_create_task(coro)
|
||||||
|
created_tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
monkeypatch.setattr(usage_stats_module.asyncio, "create_task", _track_task)
|
||||||
|
|
||||||
|
stats = UsageStats()
|
||||||
|
|
||||||
|
new_path = settings_dir / "stats" / UsageStats.STATS_FILENAME
|
||||||
|
assert new_path.exists(), "Stats file should be migrated to new location"
|
||||||
|
assert not old_path.exists(), "Old stats file should be removed after migration"
|
||||||
|
assert stats.stats["total_executions"] == 3
|
||||||
|
assert stats.stats["loras"]["lora-hash"]["total"] == 3
|
||||||
|
|
||||||
|
await _finalize_usage_stats(created_tasks)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_usage_stats_uses_new_location_directly(tmp_path, monkeypatch):
|
||||||
|
settings_dir = tmp_path / "settings"
|
||||||
|
settings_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module, "get_settings_dir", lambda create=True: str(settings_dir))
|
||||||
|
loras_root = tmp_path / "loras"
|
||||||
|
loras_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
monkeypatch.setattr(usage_stats_module.config, "loras_roots", [str(loras_root)])
|
||||||
|
|
||||||
|
new_data = {
|
||||||
|
"checkpoints": {},
|
||||||
|
"loras": {},
|
||||||
|
"embeddings": {},
|
||||||
|
"total_executions": 7,
|
||||||
|
"last_save_time": 200.0,
|
||||||
|
}
|
||||||
|
new_path = settings_dir / "stats" / UsageStats.STATS_FILENAME
|
||||||
|
new_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
new_path.write_text(json.dumps(new_data), encoding="utf-8")
|
||||||
|
|
||||||
|
created_tasks = []
|
||||||
|
real_create_task = usage_stats_module.asyncio.create_task
|
||||||
|
|
||||||
|
def _track_task(coro):
|
||||||
|
task = real_create_task(coro)
|
||||||
|
created_tasks.append(task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
monkeypatch.setattr(usage_stats_module.asyncio, "create_task", _track_task)
|
||||||
|
|
||||||
|
stats = UsageStats()
|
||||||
|
|
||||||
|
assert stats.stats["total_executions"] == 7
|
||||||
|
assert not loras_root.joinpath(UsageStats.STATS_FILENAME).exists()
|
||||||
|
|
||||||
|
await _finalize_usage_stats(created_tasks)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user