Compare commits

..

30 Commits

Author SHA1 Message Date
Will Miao
dd1cdce16d fix(ui): unify context menu ordering and add visual section separators across all menus 2026-06-10 22:18:43 +08:00
Will Miao
a9e0e7dc8d feat(recipe): add reimport UI with context menus, progress display, and i18n
- Single recipe right-click menu: Re-import from Source
- Bulk context menu: Re-import Metadata for Selected
- Progress overlay with LoadingManager for single and bulk operations
- Virtual scroller data lookup (replaces fragile DOM querySelector)
- Fix dynamic import path for resetAndReload on recipe pages
- Add translation keys for all 9 supported languages
2026-06-10 21:51:04 +08:00
Will Miao
b302d1db7d feat(recipe): add reimport endpoint to re-import recipe from source URL
Adds POST /api/lm/recipe/{recipe_id}/reimport that atomically:
1. Reads the existing recipe to extract source_url and user edits
2. Deletes the old recipe files and cache entries
3. Re-downloads the image from CivitAI, re-parses EXIF metadata
4. Carries over user edits (title, tags, favorite) and timestamps
2026-06-10 21:50:43 +08:00
Will Miao
7cbddd9cf7 fix(recipe): fall back to original image for metadata extraction when optimized lacks embedded data (#968)
When CivitAI API returns meta=null and the optimized CDN image has no
embedded generation parameters (e.g. PNG tEXt chunks stripped by
Cloudflare Images), download the original image as fallback to recover
full recipe metadata (prompt, seed, LoRAs, etc.).

Also fixes Chrome password manager popping up on recipe save by adding
autocomplete="new-password" to the settings API key and proxy password
fields.
2026-06-10 15:06:56 +08:00
Will Miao
cb8c699224 chore(template): update template workflow 2026-06-10 15:01:48 +08:00
Will Miao
451f74b874 fix(ui): return minWidth/minHeight from autocomplete text widget factory for proper node initial sizing 2026-06-09 15:21:45 +08:00
pixelpaws
a1d248baa6 Merge pull request #966 from willmiao/design-token-system-phase4
Design token system phase4
2026-06-09 14:37:02 +08:00
Will Miao
18577fa336 refactor(phase-4): standardize remaining transitions and box-shadows
- Replace all remaining 'transition: all' with specific token-based transitions
- Replace 80+ hardcoded box-shadow rgba values with semantic tokens
- Add new tokens: --shadow-side, --shadow-elevated, --shadow-dialog, --shadow-inset-top
- Update dark theme overrides for new shadow tokens
- 32 files changed, net +8 lines (more consistent, less duplication)
2026-06-09 14:27:53 +08:00
Will Miao
5797ce9408 feat(phase-4): visual polish — font stack, shadow system, transitions, micro-interactions
Phase 4: Visual Polish

4.1 Font Stack Upgrade:
- Add --font-display token for headings
- Replace all hardcoded font-family: monospace with var(--font-mono)
- Replace hardcoded 'Segoe UI' stack with var(--font-body)

4.2 Shadow Elevation System:
- Add --shadow-2xl, --shadow-card/dropdown/modal/toast/header/dark-lg tokens
- Replace hardcoded shadows in header, menu, banner, shared, recipe-modal,
  progress-panel, import-modal, alphabet-bar with semantic tokens
- Add dark theme shadow overrides with increased opacity

4.3 Transitions & Micro-interactions:
- Replace transition: all with specified properties (performance)
- Use --transition-fast/base/slow tokens instead of hardcoded 0.2s/0.3s
- Add :active scale feedback to modal buttons
- Enhance card hover with box-shadow + border-color lift

4.4 Dark Theme Refinement:
- Elevated shadow opacity for dark theme visibility

4.5 Density:
- Standardize container padding with --space-2 token

21 files changed
2026-06-09 14:07:36 +08:00
pixelpaws
826f06255a Merge pull request #964 from willmiao/design-token-system
Design token system phase1
2026-06-09 11:38:31 +08:00
Will Miao
84e16b5c5b refactor(css): remove hardcoded background/border from modal sections - use design tokens instead 2026-06-09 09:52:11 +08:00
Will Miao
eb22054580 fix: add --surface-subtle token, restore info grouping, and apply theme-aware favorite color
- Add --surface-subtle (oklch 3% opacity) to replace rgba(0,0,0,0.03)
- Fix info items, creator-info, civitai-view, modal-send-btn, header-actions
  to use --surface-subtle instead of --surface-hover
- Keep true hover states on --surface-hover
- Use light #d4a017 / dark #ffc107 for --favorite-color based on theme
- Replace hardcoded #ffc107 and #d4a017 with var(--favorite-color)
2026-06-09 09:27:11 +08:00
Will Miao
08afb05ece refactor: normalize components in Phase 2
- Unify button styles (padding, gap, border-radius, hover states) in _base.css
- Fix .secondary-btn syntax error (extra space in var())
- Remove duplicated .card-actions in card.css
- Replace hardcoded #f0f0f0 with --surface-hover token
- Replace #ffc107 with accessible #d4a017 for favorite stars
- Replace hardcoded rgba shadows with semantic --shadow-* tokens in layout.css
- Replace hardcoded rgba(0,0,0,0.03)/rgba(255,255,255,0.03) with --surface-hover
- Remove redundant [data-theme=dark] overrides by using theme-aware tokens
- Replace .dropdown-main hardcoded border with --border-color token
2026-06-09 09:26:28 +08:00
Will Miao
f51f125cf1 feat: introduce design token system foundation
- Add semantic OKLch color tokens with light/dark themes
- Add typography, spacing, effects, breakpoints, z-index tokens
- Refactor base.css with backward-compatible aliases
- Add prefers-reduced-motion support
- Add MIGRATION.md for Phase 2 component audit
2026-06-09 09:26:28 +08:00
Will Miao
24b2078f21 fix: batch URL download UI polish - hint text, label, and i18n (#936)
- Add .input-hint helper text below textarea guiding multi-URL input
- Update label to CivitAI URL(s): for batch-agnostic hint
- Add urlHint locale key across all 10 languages
- Remove unused url locale key
2026-06-09 07:57:33 +08:00
Will Miao
130fb5d2d5 fix: batch URL download dedup by modelId+modelVersionId composite key (#936)
When batch-downloading different versions of the same model, dedup by
modelId alone discards the second URL. Use modelId:modelVersionId as
the dedup key so users can download, e.g., latest + a specific version.
2026-06-09 07:02:56 +08:00
Will Miao
23c6863a3a fix: batch URL download i18n and CSS polish (#936)
- Add common.actions.remove/change translation keys across all locales
- Remove hardcoded #e74c3c error colors, use --lora-error CSS variable
2026-06-08 21:28:24 +08:00
Will Miao
c0e2578640 feat(ui): add adaptive expand/collapse for Additional Notes section (#962) 2026-06-08 20:52:41 +08:00
Will Miao
e3c812367e fix(ui): cap lora widget height and enable wheel scroll in Node 2.0 mode (#959)
- Add 'Node 2.0: Maximum visible LoRA entries' setting (default 12)
- Apply max-height to loras container in Vue mode to prevent unbounded growth
- Add enableListWheelScroll: window capture-phase wheel hook so scroll
  inside the widget scrolls the list instead of zooming the canvas
2026-06-08 16:19:08 +08:00
Will Miao
4d239008a6 fix(update): respect hide_early_access_updates in refresh toast count
The refresh_model_updates handler was calling record.has_update() with
default hide_early_access=False, causing the toast to report early-access
updates that the Updates filter (which uses the user's hide_early_access
setting) would then hide. This resulted in misleading "Found N updates"
toasts followed by an empty Updates view.

Now the handler reads hide_early_access_updates from settings and passes
it to has_update(), matching the behavior of _serialize_record and
_annotate_update_flags.
2026-06-08 13:58:21 +08:00
Will Miao
00177a06d0 fix(ui): keep autocomplete text widget at max-height on node resize in Vue mode 2026-06-08 10:49:04 +08:00
Will Miao
568daa351e Revert "Merge pull request #959 from id-fa/fix/lora-loader-list-scroll-nodes2"
This reverts commit 01dac57c35, reversing
changes made to 62f9e3f44a.
2026-06-07 17:25:30 +08:00
Will Miao
5a4664fa12 Merge pull request #936 from 1756141021/feat/batch-url-download
feat: batch URL download for LoRA models
2026-06-06 20:22:52 +08:00
Will Miao
dd5b213adc fix(ui): make autocomplete text widget scrollable in Nodes 2.0 mode
In Vue/Node 2.0 mode, the AutocompleteTextWidget's textarea wheel events were intercepted by TransformPane @wheel.capture before reaching the @wheel handler, causing canvas zoom instead of text scrolling.

- Add lm-wheel-scrollable class in Vue mode to hook into the window capture-phase handler (enableListWheelScroll) which scrolls the textarea manually before TransformPane can react.
- Add maxHeight prop and container max-height for Lora Loader/Stacker/WanVideo nodes (modelType === 'loras'), matching canvas mode's height cap. Prompt/Text nodes remain uncapped.
2026-06-06 08:12:09 +08:00
Will Miao
d9ee9b3155 fix(utils): catch MemoryError in read_safetensors_metadata for non-safetensors files 2026-06-06 07:35:36 +08:00
pixelpaws
01dac57c35 Merge pull request #959 from id-fa/fix/lora-loader-list-scroll-nodes2
fix(ui): make Lora Loader list scrollable in Nodes 2.0 mode
2026-06-06 07:33:19 +08:00
id-fa
7f92d09239 fix(ui): make Lora Loader list scrollable in Nodes 2.0 mode
In Nodes 2.0 / Vue node mode the Lora Loader list could not be capped
and the node grew to show every row, unlike classic mode which fixes the
list area to 12 rows. The Vue layout engine measures the rendered DOM, so
CSS variables and computeLayoutSize alone were ignored.

- Physically cap the container via max-height so the rendered element is
  bounded to the 12-row height; extra rows scroll (overflow: auto).
- Report the capped height through computeSize / computeLayoutSize /
  getHeight / getMinHeight so the node background matches the list.
- Add enableListWheelScroll: a window capture-phase wheel hook that scrolls
  the hovered list instead of letting ComfyUI zoom the canvas, which fires
  on the document/canvas in capture and beat a container-level listener.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:29:01 +09:00
Will Miao
62f9e3f44a fix(scripts): use platformdirs for cross-platform settings path resolution
Both restore_suffixed_filenames.py and migrate_legacy_metadata.py
hardcoded Path.home() / '.config' / APP_NAME for finding settings.json,
which only works on Linux. On Windows this resolves to the wrong path
(~/.config/ instead of %LOCALAPPDATA%).

Replace the hand-rolled fallback with platformdirs.user_config_dir(),
which correctly resolves to the OS-appropriate config directory on all
platforms (Windows: %%LOCALAPPDATA%%, macOS: ~/Library/Application Support,
Linux: ~/.config). The portable mode check (settings.json in repo root
with use_portable_settings: true) is preserved unchanged.
2026-06-04 07:17:53 +08:00
willmiao
e55895786d docs: auto-update supporters list in README 2026-06-03 14:30:44 +00:00
hein
4e3ede23b7 feat: batch URL download for LoRA models
Add multi-URL batch download support to the download modal.
Users can paste multiple CivitAI URLs (one per line) in a textarea,
preview all parsed models in a compact list, optionally change versions
per model, select a unified download path, and batch download sequentially.

Single URL behavior is preserved unchanged.

Changes:
- Replace single-line input with textarea for multi-URL input
- Add batch preview step with compact list (thumbnail, version, size)
- Per-item version editing via existing version selector
- Batch download with WebSocket progress tracking (reuses existing infra)
- URL deduplication by model ID, preserving paste order
- Invalid URLs shown inline with remove option
- Fix: prevent click listener accumulation in showVersionStep

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-20 11:37:36 +08:00
86 changed files with 2490 additions and 689 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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...",
@@ -690,6 +692,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": "Metadaten der Auswahl 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 +740,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 +868,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": {
@@ -1014,9 +1025,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 +1236,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...",
@@ -1713,6 +1726,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."
}, },

View File

@@ -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...",
@@ -690,6 +692,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 Metadata for Selected",
"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 +740,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 +868,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": {
@@ -1014,9 +1025,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 +1236,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...",
@@ -1713,6 +1726,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."
}, },

View File

@@ -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...",
@@ -690,6 +692,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 metadatos de la selección",
"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 +740,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 +868,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": {
@@ -1014,9 +1025,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 +1236,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...",
@@ -1713,6 +1726,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."
}, },

View File

@@ -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...",
@@ -690,6 +692,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 les métadonnées de la sélection",
"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 +740,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 +868,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": {
@@ -1014,9 +1025,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 +1236,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...",
@@ -1713,6 +1726,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."
}, },

View File

@@ -16,7 +16,9 @@
"help": "עזרה", "help": "עזרה",
"add": "הוספה", "add": "הוספה",
"close": "סגור", "close": "סגור",
"menu": "תפריט" "menu": "תפריט",
"remove": "הסר",
"change": "שנה"
}, },
"status": { "status": {
"loading": "טוען...", "loading": "טוען...",
@@ -690,6 +692,7 @@
"copyAll": "העתק את כל התחבירים", "copyAll": "העתק את כל התחבירים",
"refreshAll": "רענן את כל המטא-דאטה", "refreshAll": "רענן את כל המטא-דאטה",
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים", "repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
"reimportMetadata": "ייבא מחדש מטא-דאטה עבור הנבחרים",
"checkUpdates": "בדוק עדכונים לבחירה", "checkUpdates": "בדוק עדכונים לבחירה",
"moveAll": "העבר הכל לתיקייה", "moveAll": "העבר הכל לתיקייה",
"autoOrganize": "ארגן אוטומטית נבחרים", "autoOrganize": "ארגן אוטומטית נבחרים",
@@ -737,6 +740,7 @@
"setContentRating": "הגדר דירוג תוכן", "setContentRating": "הגדר דירוג תוכן",
"moveToFolder": "העבר לתיקייה", "moveToFolder": "העבר לתיקייה",
"repairMetadata": "תיקון מטא-דאטה", "repairMetadata": "תיקון מטא-דאטה",
"reimportMetadata": "ייבא מחדש ממקור",
"excludeModel": "החרג מודל", "excludeModel": "החרג מודל",
"restoreModel": "שחזור מודל", "restoreModel": "שחזור מודל",
"deleteModel": "מחק מודל", "deleteModel": "מחק מודל",
@@ -864,6 +868,13 @@
"skipped": "המתכון כבר בגרסה העדכנית ביותר, אין צורך בתיקון", "skipped": "המתכון כבר בגרסה העדכנית ביותר, אין צורך בתיקון",
"failed": "תיקון המתכון נכשל: {message}", "failed": "תיקון המתכון נכשל: {message}",
"missingId": "לא ניתן לתקן את המתכון: חסר מזהה מתכון" "missingId": "לא ניתן לתקן את המתכון: חסר מזהה מתכון"
},
"reimport": {
"starting": "מייבא מתכון מחדש מהמקור...",
"success": "המתכון יובא מחדש בהצלחה",
"noSourceUrl": "למתכון אין כתובת מקור, לא ניתן לייבא מחדש",
"failed": "ייבוא המתכון מחדש נכשל: {message}",
"missingId": "לא ניתן לייבא מחדש: חסר מזהה מתכון"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "הערות נשמרו בהצלחה", "saved": "הערות נשמרו בהצלחה",
"saveFailed": "שמירת ההערות נכשלה" "saveFailed": "שמירת ההערות נכשלה",
"showMore": "הצג עוד",
"showLess": "הצג פחות"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "הוסף פרמטר קבוע מראש...", "addPresetParameter": "הוסף פרמטר קבוע מראש...",
@@ -1713,6 +1726,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 ברירת מחדל בהגדרות."
}, },

View File

@@ -16,7 +16,9 @@
"help": "ヘルプ", "help": "ヘルプ",
"add": "追加", "add": "追加",
"close": "閉じる", "close": "閉じる",
"menu": "メニュー" "menu": "メニュー",
"remove": "削除",
"change": "変更"
}, },
"status": { "status": {
"loading": "読み込み中...", "loading": "読み込み中...",
@@ -690,6 +692,7 @@
"copyAll": "すべての構文をコピー", "copyAll": "すべての構文をコピー",
"refreshAll": "すべてのメタデータを更新", "refreshAll": "すべてのメタデータを更新",
"repairMetadata": "選択したレシピのメタデータを修復", "repairMetadata": "選択したレシピのメタデータを修復",
"reimportMetadata": "選択したレシピを再インポート",
"checkUpdates": "選択項目の更新を確認", "checkUpdates": "選択項目の更新を確認",
"moveAll": "すべてをフォルダに移動", "moveAll": "すべてをフォルダに移動",
"autoOrganize": "自動整理を実行", "autoOrganize": "自動整理を実行",
@@ -737,6 +740,7 @@
"setContentRating": "コンテンツレーティングを設定", "setContentRating": "コンテンツレーティングを設定",
"moveToFolder": "フォルダに移動", "moveToFolder": "フォルダに移動",
"repairMetadata": "メタデータを修復", "repairMetadata": "メタデータを修復",
"reimportMetadata": "ソースから再インポート",
"excludeModel": "モデルを除外", "excludeModel": "モデルを除外",
"restoreModel": "モデルを復元", "restoreModel": "モデルを復元",
"deleteModel": "モデルを削除", "deleteModel": "モデルを削除",
@@ -864,6 +868,13 @@
"skipped": "レシピはすでに最新バージョンです。修復は不要です", "skipped": "レシピはすでに最新バージョンです。修復は不要です",
"failed": "レシピの修復に失敗しました: {message}", "failed": "レシピの修復に失敗しました: {message}",
"missingId": "レシピを修復できません: レシピIDがありません" "missingId": "レシピを修復できません: レシピIDがありません"
},
"reimport": {
"starting": "ソースからレシピを再インポート中...",
"success": "レシピの再インポートが完了しました",
"noSourceUrl": "レシピにソースURLがありません。再インポートできません",
"failed": "レシピの再インポートに失敗しました: {message}",
"missingId": "レシピを再インポートできません: レシピIDがありません"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "メモが正常に保存されました", "saved": "メモが正常に保存されました",
"saveFailed": "メモの保存に失敗しました" "saveFailed": "メモの保存に失敗しました",
"showMore": "もっと見る",
"showLess": "折りたたむ"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "プリセットパラメータを追加...", "addPresetParameter": "プリセットパラメータを追加...",
@@ -1713,6 +1726,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 ルートを設定してください。"
}, },

View File

@@ -16,7 +16,9 @@
"help": "도움말", "help": "도움말",
"add": "추가", "add": "추가",
"close": "닫기", "close": "닫기",
"menu": "메뉴" "menu": "메뉴",
"remove": "제거",
"change": "변경"
}, },
"status": { "status": {
"loading": "로딩 중...", "loading": "로딩 중...",
@@ -690,6 +692,7 @@
"copyAll": "모든 문법 복사", "copyAll": "모든 문법 복사",
"refreshAll": "모든 메타데이터 새로고침", "refreshAll": "모든 메타데이터 새로고침",
"repairMetadata": "선택한 레시피 메타데이터 복구", "repairMetadata": "선택한 레시피 메타데이터 복구",
"reimportMetadata": "선택한 레시피 다시 가져오기",
"checkUpdates": "선택 항목 업데이트 확인", "checkUpdates": "선택 항목 업데이트 확인",
"moveAll": "모두 폴더로 이동", "moveAll": "모두 폴더로 이동",
"autoOrganize": "자동 정리 선택", "autoOrganize": "자동 정리 선택",
@@ -737,6 +740,7 @@
"setContentRating": "콘텐츠 등급 설정", "setContentRating": "콘텐츠 등급 설정",
"moveToFolder": "폴더로 이동", "moveToFolder": "폴더로 이동",
"repairMetadata": "메타데이터 복구", "repairMetadata": "메타데이터 복구",
"reimportMetadata": "소스에서 다시 가져오기",
"excludeModel": "모델 제외", "excludeModel": "모델 제외",
"restoreModel": "모델 복원", "restoreModel": "모델 복원",
"deleteModel": "모델 삭제", "deleteModel": "모델 삭제",
@@ -864,6 +868,13 @@
"skipped": "레시피가 이미 최신 버전입니다. 복구가 필요하지 않습니다", "skipped": "레시피가 이미 최신 버전입니다. 복구가 필요하지 않습니다",
"failed": "레시피 복구 실패: {message}", "failed": "레시피 복구 실패: {message}",
"missingId": "레시피를 복구할 수 없음: 레시피 ID 누락" "missingId": "레시피를 복구할 수 없음: 레시피 ID 누락"
},
"reimport": {
"starting": "소스에서 레시피를 다시 가져오는 중...",
"success": "레시피를 다시 가져왔습니다",
"noSourceUrl": "레시피에 소스 URL이 없어 다시 가져올 수 없습니다",
"failed": "레시피 다시 가져오기 실패: {message}",
"missingId": "레시피를 다시 가져올 수 없음: 레시피 ID 누락"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "메모가 성공적으로 저장됨", "saved": "메모가 성공적으로 저장됨",
"saveFailed": "메모 저장 실패" "saveFailed": "메모 저장 실패",
"showMore": "더 보기",
"showLess": "접기"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "프리셋 매개변수 추가...", "addPresetParameter": "프리셋 매개변수 추가...",
@@ -1713,6 +1726,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 루트를 설정하세요."
}, },

View File

@@ -16,7 +16,9 @@
"help": "Справка", "help": "Справка",
"add": "Добавить", "add": "Добавить",
"close": "Закрыть", "close": "Закрыть",
"menu": "Меню" "menu": "Меню",
"remove": "Удалить",
"change": "Изменить"
}, },
"status": { "status": {
"loading": "Загрузка...", "loading": "Загрузка...",
@@ -690,6 +692,7 @@
"copyAll": "Копировать весь синтаксис", "copyAll": "Копировать весь синтаксис",
"refreshAll": "Обновить все метаданные", "refreshAll": "Обновить все метаданные",
"repairMetadata": "Восстановить метаданные для выбранных", "repairMetadata": "Восстановить метаданные для выбранных",
"reimportMetadata": "Переимпортировать метаданные для выбранных",
"checkUpdates": "Проверить обновления для выбранных", "checkUpdates": "Проверить обновления для выбранных",
"moveAll": "Переместить все в папку", "moveAll": "Переместить все в папку",
"autoOrganize": "Автоматически организовать выбранные", "autoOrganize": "Автоматически организовать выбранные",
@@ -737,6 +740,7 @@
"setContentRating": "Установить рейтинг контента", "setContentRating": "Установить рейтинг контента",
"moveToFolder": "Переместить в папку", "moveToFolder": "Переместить в папку",
"repairMetadata": "Восстановить метаданные", "repairMetadata": "Восстановить метаданные",
"reimportMetadata": "Переимпортировать из источника",
"excludeModel": "Исключить модель", "excludeModel": "Исключить модель",
"restoreModel": "Восстановить модель", "restoreModel": "Восстановить модель",
"deleteModel": "Удалить модель", "deleteModel": "Удалить модель",
@@ -864,6 +868,13 @@
"skipped": "Рецепт уже последней версии, восстановление не требуется", "skipped": "Рецепт уже последней версии, восстановление не требуется",
"failed": "Не удалось восстановить рецепт: {message}", "failed": "Не удалось восстановить рецепт: {message}",
"missingId": "Не удалось восстановить рецепт: отсутствует ID рецепта" "missingId": "Не удалось восстановить рецепт: отсутствует ID рецепта"
},
"reimport": {
"starting": "Переимпорт рецепта из источника...",
"success": "Рецепт успешно переимпортирован",
"noSourceUrl": "У рецепта нет URL источника, переимпорт невозможен",
"failed": "Не удалось переимпортировать рецепт: {message}",
"missingId": "Невозможно переимпортировать рецепт: отсутствует ID"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "Заметки успешно сохранены", "saved": "Заметки успешно сохранены",
"saveFailed": "Не удалось сохранить заметки" "saveFailed": "Не удалось сохранить заметки",
"showMore": "Показать больше",
"showLess": "Свернуть"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "Добавить предустановленный параметр...", "addPresetParameter": "Добавить предустановленный параметр...",
@@ -1713,6 +1726,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 по умолчанию в настройках."
}, },

View File

@@ -16,7 +16,9 @@
"help": "帮助", "help": "帮助",
"add": "添加", "add": "添加",
"close": "关闭", "close": "关闭",
"menu": "菜单" "menu": "菜单",
"remove": "移除",
"change": "更换"
}, },
"status": { "status": {
"loading": "加载中...", "loading": "加载中...",
@@ -690,6 +692,7 @@
"copyAll": "复制所选中语法", "copyAll": "复制所选中语法",
"refreshAll": "刷新所选中元数据", "refreshAll": "刷新所选中元数据",
"repairMetadata": "修复所选中元数据", "repairMetadata": "修复所选中元数据",
"reimportMetadata": "重新导入所选配方元数据",
"checkUpdates": "检查所选更新", "checkUpdates": "检查所选更新",
"moveAll": "移动所选中到文件夹", "moveAll": "移动所选中到文件夹",
"autoOrganize": "自动整理所选模型", "autoOrganize": "自动整理所选模型",
@@ -737,6 +740,7 @@
"setContentRating": "设置内容评级", "setContentRating": "设置内容评级",
"moveToFolder": "移动到文件夹", "moveToFolder": "移动到文件夹",
"repairMetadata": "修复元数据", "repairMetadata": "修复元数据",
"reimportMetadata": "从源重新导入",
"excludeModel": "排除模型", "excludeModel": "排除模型",
"restoreModel": "恢复模型", "restoreModel": "恢复模型",
"deleteModel": "删除模型", "deleteModel": "删除模型",
@@ -864,6 +868,13 @@
"skipped": "配方已是最新版本,无需修复", "skipped": "配方已是最新版本,无需修复",
"failed": "修复配方失败:{message}", "failed": "修复配方失败:{message}",
"missingId": "无法修复配方:缺少配方 ID" "missingId": "无法修复配方:缺少配方 ID"
},
"reimport": {
"starting": "正在从源重新导入配方...",
"success": "配方已从源重新导入成功",
"noSourceUrl": "配方没有源URL无法重新导入",
"failed": "重新导入配方失败:{message}",
"missingId": "无法重新导入配方缺少配方ID"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "备注保存成功", "saved": "备注保存成功",
"saveFailed": "备注保存失败" "saveFailed": "备注保存失败",
"showMore": "展开",
"showLess": "收起"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "添加预设参数...", "addPresetParameter": "添加预设参数...",
@@ -1713,6 +1726,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 根目录。"
}, },

View File

@@ -16,7 +16,9 @@
"help": "說明", "help": "說明",
"add": "新增", "add": "新增",
"close": "關閉", "close": "關閉",
"menu": "選單" "menu": "選單",
"remove": "移除",
"change": "更換"
}, },
"status": { "status": {
"loading": "載入中...", "loading": "載入中...",
@@ -690,6 +692,7 @@
"copyAll": "複製全部語法", "copyAll": "複製全部語法",
"refreshAll": "刷新全部 metadata", "refreshAll": "刷新全部 metadata",
"repairMetadata": "修復所選中元數據", "repairMetadata": "修復所選中元數據",
"reimportMetadata": "重新匯入所選配方元數據",
"checkUpdates": "檢查所選更新", "checkUpdates": "檢查所選更新",
"moveAll": "全部移動到資料夾", "moveAll": "全部移動到資料夾",
"autoOrganize": "自動整理所選模型", "autoOrganize": "自動整理所選模型",
@@ -737,6 +740,7 @@
"setContentRating": "設定內容分級", "setContentRating": "設定內容分級",
"moveToFolder": "移動到資料夾", "moveToFolder": "移動到資料夾",
"repairMetadata": "修復元數據", "repairMetadata": "修復元數據",
"reimportMetadata": "從來源重新匯入",
"excludeModel": "排除模型", "excludeModel": "排除模型",
"restoreModel": "還原模型", "restoreModel": "還原模型",
"deleteModel": "刪除模型", "deleteModel": "刪除模型",
@@ -864,6 +868,13 @@
"skipped": "配方已是最新版本,無需修復", "skipped": "配方已是最新版本,無需修復",
"failed": "修復配方失敗:{message}", "failed": "修復配方失敗:{message}",
"missingId": "無法修復配方:缺少配方 ID" "missingId": "無法修復配方:缺少配方 ID"
},
"reimport": {
"starting": "正在從來源重新匯入配方...",
"success": "配方已從來源重新匯入成功",
"noSourceUrl": "配方沒有來源URL無法重新匯入",
"failed": "重新匯入配方失敗:{message}",
"missingId": "無法重新匯入配方缺少配方ID"
} }
}, },
"batchImport": { "batchImport": {
@@ -1014,9 +1025,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 +1236,9 @@
}, },
"notes": { "notes": {
"saved": "備註已儲存", "saved": "備註已儲存",
"saveFailed": "儲存備註失敗" "saveFailed": "儲存備註失敗",
"showMore": "展開",
"showLess": "收起"
}, },
"usageTips": { "usageTips": {
"addPresetParameter": "新增預設參數...", "addPresetParameter": "新增預設參數...",
@@ -1713,6 +1726,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 根目錄。"
}, },

View File

@@ -2016,10 +2016,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(

View File

@@ -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,116 @@ 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"):
if key in old_recipe and old_recipe[key]:
user_edits[key] = old_recipe[key]
if "tags" in user_edits and not isinstance(user_edits["tags"], list):
del user_edits["tags"]
old_created = old_recipe.get("created_date")
old_modified = old_recipe.get("modified")
await self._persistence_service.delete_recipe(
recipe_scanner=recipe_scanner, recipe_id=recipe_id
)
async with self._import_semaphore:
import_response = await self._do_import_from_url(
source_path, recipe_scanner
)
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,
)
timestamp_updates: dict[str, Any] = {}
if old_created is not None:
timestamp_updates["created_date"] = old_created
if old_modified is not None:
timestamp_updates["modified"] = old_modified
if new_recipe_id and timestamp_updates:
try:
await recipe_scanner.update_recipe_metadata(
new_recipe_id, timestamp_updates
)
except Exception as exc:
self._logger.warning(
"Re-import succeeded but failed to preserve "
"timestamps 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 +1018,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 +1431,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 +1508,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
@@ -1550,7 +1669,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 +1707,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).

View File

@@ -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"
),
) )

View File

@@ -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,

View File

@@ -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 {}

View File

@@ -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]:

View File

@@ -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]:

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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 */

View File

@@ -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 {

View File

@@ -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,
background: var(--lora-surface-hover, oklch(95% 0.02 256)); .header-search .search-filter-toggle:focus-visible {
color: var(--lora-accent); background: var(--lora-surface-hover, oklch(95% 0.02 256));
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;
} }

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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 {

View File

@@ -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 */

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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);
}

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,7 @@
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);
} }
.sidebar-header:hover { .sidebar-header:hover {
@@ -120,7 +120,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;
@@ -174,7 +174,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 +298,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 +331,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 +364,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 +382,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 +406,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 +614,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 +649,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 +685,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 +702,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 {

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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);
} }

View 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)

View File

@@ -0,0 +1,8 @@
:root {
--bp-mobile: 768px;
--bp-tablet: 1024px;
--bp-desktop: 1400px;
--bp-wide: 1920px;
--bp-ultrawide: 2150px;
--bp-4k: 3000px;
}

View 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;
}

View 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);
}

View File

@@ -0,0 +1,6 @@
@import 'colors.css';
@import 'typography.css';
@import 'spacing.css';
@import 'effects.css';
@import 'breakpoints.css';
@import 'z-index.css';

View 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);
}

View 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;
}

View 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;
}

View File

@@ -42,10 +42,14 @@ 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';
}
if (sendToWorkflowAppendItem) { if (sendToWorkflowAppendItem) {
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none'; sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
@@ -264,6 +268,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);

View File

@@ -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: true });
} 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

View File

@@ -510,7 +510,12 @@ export async function showModelModal(model, modelType) {
</div> </div>
${typeSpecificContent} ${typeSpecificContent}
<div class="info-item notes"> <div class="info-item notes">
<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> <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>
<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>
@@ -837,12 +842,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');

View File

@@ -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
} }
}; };
@@ -657,6 +658,87 @@ 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`
);
} else {
state.loadingManager.hide();
showToast('toast.recipes.reimportBulkFailed', {}, 'error');
}
const { resetAndReload: recipeResetAndReload } = await import('../api/recipeApi.js');
recipeResetAndReload(false, { preserveScroll: true });
this.clearSelection();
} 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');

View File

@@ -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,30 +171,104 @@ 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);
try { if (urls.length === 0) {
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions')); errorElement.textContent = translate('modals.download.errors.invalidUrl');
return;
this.modelId = this.extractModelId(url);
if (!this.modelId) {
throw new Error(translate('modals.download.errors.invalidUrl'));
}
await this.retrieveVersionsForModel(this.modelId, this.source);
// If we have a version ID from URL, pre-select it
if (this.modelVersionId) {
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
}
this.showVersionStep();
} catch (error) {
errorElement.textContent = error.message;
} finally {
this.loadingManager.hide();
} }
if (urls.length === 1) {
this.isBatchMode = false;
try {
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
this.modelId = this.extractModelId(urls[0]);
if (!this.modelId) {
throw new Error(translate('modals.download.errors.invalidUrl'));
}
await this.retrieveVersionsForModel(this.modelId, this.source);
if (this.modelVersionId) {
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
}
this.showVersionStep();
} catch (error) {
errorElement.textContent = error.message;
} finally {
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 (!this.currentVersion) { // If editing a batch item's version, save and return to batch preview
showToast('toast.loras.pleaseSelectVersion', {}, 'error'); 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; return;
} }
const existsLocally = this.currentVersion.existsLocally; // In single-URL mode, validate version selection
if (existsLocally) { if (!this.isBatchMode) {
showToast('toast.loras.versionExists', {}, 'info'); if (!this.currentVersion) {
return; showToast('toast.loras.pleaseSelectVersion', {}, 'error');
return;
}
if (this.currentVersion.existsLocally) {
showToast('toast.loras.versionExists', {}, 'info');
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,14 +808,123 @@ 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';
document.getElementById('urlStep').style.display = 'block'; if (this.isBatchMode && this.editingBatchIndex >= 0) {
this.editingBatchIndex = -1;
this.showBatchPreviewStep();
} else {
document.getElementById('urlStep').style.display = 'block';
}
} }
backToVersions() { backToVersions() {
document.getElementById('locationStep').style.display = 'none'; document.getElementById('locationStep').style.display = 'none';
document.getElementById('versionStep').style.display = 'block'; if (this.isBatchMode) {
document.getElementById('batchPreviewStep').style.display = 'block';
} else {
document.getElementById('versionStep').style.display = 'block';
}
} }
closeModal() { closeModal() {
@@ -727,34 +944,120 @@ 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();
} }
const fileParams = this.selectedFile ? { if (!this.isBatchMode) {
type: 'Model', const fileParams = this.selectedFile ? {
format: this.selectedFile.metadata?.format || 'SafeTensor', type: 'Model',
size: this.selectedFile.metadata?.size || 'full', format: this.selectedFile.metadata?.format || 'SafeTensor',
fp: this.selectedFile.metadata?.fp, size: this.selectedFile.metadata?.size || 'full',
} : null; fp: this.selectedFile.metadata?.fp,
} : null;
return this.executeDownloadWithProgress({ return this.executeDownloadWithProgress({
modelId: this.modelId, modelId: this.modelId,
versionId: this.currentVersion.id, versionId: this.currentVersion.id,
versionName: this.currentVersion.name, versionName: this.currentVersion.name,
modelRoot, modelRoot,
targetFolder, targetFolder,
useDefaultPaths, useDefaultPaths,
source: this.source, source: this.source,
fileParams, fileParams,
closeModal: true, closeModal: true,
});
}
// 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, {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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">

View File

@@ -10,15 +10,25 @@
{% 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-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>

View File

@@ -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>

View File

@@ -5,7 +5,9 @@
ref="textareaRef" ref="textareaRef"
:placeholder="placeholder" :placeholder="placeholder"
:spellcheck="spellcheck ?? false" :spellcheck="spellcheck ?? false"
:class="['text-input', { 'vue-dom-mode': isVueDomMode }]" :class="['text-input', { 'vue-dom-mode': isVueDomMode, 'lm-wheel-scrollable': isVueDomMode }]"
:style="maxHeight && isVueDomMode ? { maxHeight: maxHeight + 'px' } : undefined"
data-capture-wheel="true"
@input="onInput" @input="onInput"
@wheel="onWheel" @wheel="onWheel"
/> />
@@ -47,6 +49,7 @@ const props = defineProps<{
placeholder?: string placeholder?: string
showPreview?: boolean showPreview?: boolean
spellcheck?: boolean spellcheck?: boolean
maxHeight?: number
}>() }>()
// Reactive ref for Vue DOM mode // Reactive ref for Vue DOM mode

View File

@@ -25,6 +25,11 @@ const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200 const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200
const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60 const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60
const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100 const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100
// Per-modelType min size hints for node initial sizing.
// These are returned from the factory so ComfyUI's _initialMinSize mechanism
// gives the node a sensible default width (and height for prompt/embeddings).
const AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT = 400
const AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT = 300
const AUTOCOMPLETE_METADATA_VERSION = 1 const AUTOCOMPLETE_METADATA_VERSION = 1
const LORA_MANAGER_WIDGET_IDS_PROPERTY = '__lm_widget_ids' const LORA_MANAGER_WIDGET_IDS_PROPERTY = '__lm_widget_ids'
@@ -546,6 +551,27 @@ function normalizeAutocompleteWidgetValues(node: any, info: any) {
} }
} }
function applyAutocompleteTextLayoutFix(
widget: any,
container: HTMLElement | undefined,
isVueMode: boolean
): void {
if (isVueMode) {
;(widget as any).computeLayoutSize = undefined
widget.computeSize = (width?: number) =>
[width ?? 200, AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT - 4]
if (container) {
container.style.minHeight = `${AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT}px`
}
} else {
delete (widget as any).computeLayoutSize
delete (widget as any).computeSize
if (container) {
container.style.minHeight = ''
}
}
}
// Listen for Vue DOM mode setting changes and dispatch custom event // Listen for Vue DOM mode setting changes and dispatch custom event
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
if (app.ui?.settings?.addEventListener) { if (app.ui?.settings?.addEventListener) {
@@ -554,7 +580,47 @@ const initVueDomModeListener = () => {
// before we read it (the event may fire before internal state updates) // before we read it (the event may fire before internal state updates)
requestAnimationFrame(() => { requestAnimationFrame(() => {
const isVueDomMode = app.ui?.settings?.getSettingValue?.('Comfy.VueNodes.Enabled') ?? false const isVueDomMode = app.ui?.settings?.getSettingValue?.('Comfy.VueNodes.Enabled') ?? false
// Dispatch custom event for Vue components to listen to
if (app.graph?.nodes) {
for (const node of app.graph.nodes) {
const textWidget = node.widgets?.find(
(w: any) => w.type === 'AUTOCOMPLETE_TEXT_LORAS'
)
if (!textWidget) continue
const container = (textWidget as any).element as HTMLElement | undefined
applyAutocompleteTextLayoutFix(textWidget, container, isVueDomMode)
}
}
requestAnimationFrame(() => {
for (const nodeEl of document.querySelectorAll('[data-node-id]')) {
const grid = nodeEl.querySelector('[data-testid="node-widgets"]') as HTMLElement | null
if (!grid) continue
const nodeId = nodeEl.getAttribute('data-node-id')
const node = app.graph?.getNodeById(nodeId as any)
if (!node) continue
const rows: string[] = []
let needsFix = false
for (const w of node.widgets ?? []) {
if (w.type === 'LORA_MANAGER_AUTOCOMPLETE_METADATA') {
rows.push('min-content')
} else if (w.name === 'loras') {
rows.push('auto')
} else if (w.name === 'text' && w.type === 'AUTOCOMPLETE_TEXT_LORAS') {
rows.push(isVueDomMode ? 'min-content' : 'auto')
needsFix = true
} else {
rows.push('auto')
}
}
if (needsFix) {
grid.style.gridTemplateRows = rows.join(' ')
}
}
})
app.canvas?.setDirty(true, true)
document.dispatchEvent(new CustomEvent('lora-manager:vue-mode-change', { document.dispatchEvent(new CustomEvent('lora-manager:vue-mode-change', {
detail: { isVueDomMode } detail: { isVueDomMode }
})) }))
@@ -655,13 +721,16 @@ function createAutocompleteTextWidgetFactory(
// Get spellcheck setting from ComfyUI settings (default: false) // Get spellcheck setting from ComfyUI settings (default: false)
const spellcheck = app.ui?.settings?.getSettingValue?.('Comfy.TextareaWidget.Spellcheck') ?? false const spellcheck = app.ui?.settings?.getSettingValue?.('Comfy.TextareaWidget.Spellcheck') ?? false
const maxHeight = modelType === 'loras' ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : undefined
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
node, node,
modelType, modelType,
placeholder: inputOptions.placeholder || widgetName, placeholder: inputOptions.placeholder || widgetName,
showPreview: true, showPreview: true,
spellcheck spellcheck,
maxHeight
}) })
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
@@ -673,11 +742,30 @@ function createAutocompleteTextWidgetFactory(
const appKey = instanceId const appKey = instanceId
vueApps.set(appKey, vueApp) vueApps.set(appKey, vueApp)
if (maxHeight) {
container.style.maxHeight = `${maxHeight}px`
container.style.minHeight = `${maxHeight}px`
}
if (modelType === 'loras') {
applyAutocompleteTextLayoutFix(
widget,
container,
typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode
)
}
widget.onRemove = createVueWidgetCleanup(vueApp, () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
vueApps.delete(appKey) vueApps.delete(appKey)
}) })
return { widget } // Return minWidth/minHeight hints so ComfyUI's _initialMinSize mechanism
// sets a sensible initial node width (and height for prompt/embeddings).
// loras modelType retains its existing height constraints (getMaxHeight: 100).
const minWidth = AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT
const minHeight = modelType === 'loras' ? undefined : AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT
return { widget, minWidth, minHeight }
} }
app.registerExtension({ app.registerExtension({

View File

@@ -11,10 +11,10 @@ import {
EMPTY_CONTAINER_HEIGHT EMPTY_CONTAINER_HEIGHT
} from "./loras_widget_utils.js"; } from "./loras_widget_utils.js";
import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js"; import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKeyboardNavigation } from "./loras_widget_events.js";
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js"; import { forwardMiddleMouseToCanvas, forwardWheelToCanvas, enableListWheelScroll } from "./utils.js";
import { PreviewTooltip } from "./preview_tooltip.js"; import { PreviewTooltip } from "./preview_tooltip.js";
import { ensureLmStyles } from "./lm_styles_loader.js"; import { ensureLmStyles } from "./lm_styles_loader.js";
import { getStrengthStepPreference } from "./settings.js"; import { getStrengthStepPreference, getLoraWidgetMaxVisibleLoras } from "./settings.js";
export function addLorasWidget(node, name, opts, callback) { export function addLorasWidget(node, name, opts, callback) {
ensureLmStyles(); ensureLmStyles();
@@ -29,6 +29,20 @@ export function addLorasWidget(node, name, opts, callback) {
// Set initial height using CSS variables approach // Set initial height using CSS variables approach
const defaultHeight = 200; const defaultHeight = 200;
// In Vue/node-2.0 mode, cap the widget height so it shows at most N entries.
// This prevents content from driving the node size beyond the cap.
// canvas/legacy mode is unaffected.
if (typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode) {
const maxLoras = getLoraWidgetMaxVisibleLoras();
const gap = 5; // flex gap from .lm-loras-container CSS
const maxH = CONTAINER_PADDING + HEADER_HEIGHT + maxLoras * LORA_ENTRY_HEIGHT + maxLoras * gap;
container.style.maxHeight = `${maxH}px`;
container.style.setProperty('--comfy-widget-max-height', `${maxH}px`);
// Window capture-phase hook: scroll the widget instead of zooming the canvas
// when the wheel is over a scrollable loras list.
enableListWheelScroll(container);
}
// Check if this is a randomizer node (lock button instead of drag handle) // Check if this is a randomizer node (lock button instead of drag handle)
const isRandomizerNode = opts?.isRandomizerNode === true; const isRandomizerNode = opts?.isRandomizerNode === true;

View File

@@ -39,6 +39,9 @@ const NEW_TAB_ZOOM_LEVEL = 0.8;
const STRENGTH_STEP_SETTING_ID = "loramanager.strength_step"; const STRENGTH_STEP_SETTING_ID = "loramanager.strength_step";
const STRENGTH_STEP_DEFAULT = 0.05; const STRENGTH_STEP_DEFAULT = 0.05;
const LORA_WIDGET_MAX_VISIBLE_SETTING_ID = "loramanager.lora_widget_max_visible_loras";
const LORA_WIDGET_MAX_VISIBLE_DEFAULT = 12;
// ============================================================================ // ============================================================================
// Helper Functions // Helper Functions
// ============================================================================ // ============================================================================
@@ -360,6 +363,32 @@ const getStrengthStepPreference = (() => {
}; };
})(); })();
const getLoraWidgetMaxVisibleLoras = (() => {
let settingsUnavailableLogged = false;
return () => {
const settingManager = app?.extensionManager?.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default max visible loras.");
settingsUnavailableLogged = true;
}
return LORA_WIDGET_MAX_VISIBLE_DEFAULT;
}
try {
const value = settingManager.get(LORA_WIDGET_MAX_VISIBLE_SETTING_ID);
return value ?? LORA_WIDGET_MAX_VISIBLE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read max visible loras setting, using default.", error);
settingsUnavailableLogged = true;
}
return LORA_WIDGET_MAX_VISIBLE_DEFAULT;
}
};
})();
// ============================================================================ // ============================================================================
// Register Extension with All Settings // Register Extension with All Settings
// ============================================================================ // ============================================================================
@@ -463,6 +492,19 @@ app.registerExtension({
tooltip: "Step size for adjusting LoRA strength via arrow buttons or keyboard (default: 0.05)", tooltip: "Step size for adjusting LoRA strength via arrow buttons or keyboard (default: 0.05)",
category: ["LoRA Manager", "LoRA Widget", "Strength Step"], category: ["LoRA Manager", "LoRA Widget", "Strength Step"],
}, },
{
id: LORA_WIDGET_MAX_VISIBLE_SETTING_ID,
name: "Node 2.0: Maximum visible LoRA entries",
type: "slider",
attrs: {
min: 3,
max: 50,
step: 1,
},
defaultValue: LORA_WIDGET_MAX_VISIBLE_DEFAULT,
tooltip: "When using Node 2.0 rendering, limit the loras widget height to show at most this many entries (default: 12). Excess entries are accessible via scrollbar.",
category: ["LoRA Manager", "LoRA Widget", "Max Visible"],
},
], ],
async setup() { async setup() {
await loadWorkflowOptions(); await loadWorkflowOptions();
@@ -549,4 +591,5 @@ export {
getUsageStatisticsPreference, getUsageStatisticsPreference,
getNewTabTemplatePreference, getNewTabTemplatePreference,
getStrengthStepPreference, getStrengthStepPreference,
getLoraWidgetMaxVisibleLoras,
}; };

View File

@@ -784,6 +784,51 @@ export function forwardWheelToCanvas(container, options = {}) {
}, { passive: false }); }, { passive: false });
} }
// Marks scrollable containers whose wheel scrolling must win over canvas zoom.
const LM_WHEEL_CLASS = 'lm-wheel-scrollable';
let lmWheelHookInstalled = false;
/**
* Keep vertical wheel scrolling inside a scrollable widget container, even in
* Nodes 2.0 / Vue mode where ComfyUI's wheel→zoom handler runs on the document
* in the capture phase (outer than any container-level listener).
* Installs a single capture-phase hook on `window` (the outermost dispatch
* point). When the wheel is over a marked, scrollable element, we manually
* scroll it and fully consume the event so canvas zoom never sees it.
*/
export function enableListWheelScroll(container) {
if (!container) return;
container.classList.add(LM_WHEEL_CLASS);
if (lmWheelHookInstalled) return;
lmWheelHookInstalled = true;
window.addEventListener('wheel', (event) => {
// Let pinch/zoom and horizontal gestures pass through.
if (event.ctrlKey) return;
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
const target = event.target;
if (!target || typeof target.closest !== 'function') return;
const el = target.closest(`.${LM_WHEEL_CLASS}`);
if (!el) return;
const canScrollY = el.scrollHeight > el.clientHeight + 1;
if (!canScrollY) return;
// Translate deltaMode to approximate pixels.
const unit = event.deltaMode === 1 ? 16
: event.deltaMode === 2 ? el.clientHeight
: 1;
el.scrollTop += event.deltaY * unit;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}, { capture: true, passive: false });
}
// Get connected Lora Pool node from pool_config input // Get connected Lora Pool node from pool_config input
export function getConnectedPoolConfigNode(node) { export function getConnectedPoolConfigNode(node) {
if (!node?.inputs) { if (!node?.inputs) {

View File

@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-5514bf46] { .autocomplete-text-widget[data-v-8555b560] {
background: transparent; background: transparent;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
.input-wrapper[data-v-5514bf46] { .input-wrapper[data-v-8555b560] {
position: relative; position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
@@ -2133,7 +2133,7 @@ to { transform: rotate(360deg);
} }
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */ /* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input[data-v-5514bf46] { .text-input[data-v-8555b560] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -2150,7 +2150,7 @@ to { transform: rotate(360deg);
} }
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */ /* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
.text-input.vue-dom-mode[data-v-5514bf46] { .text-input.vue-dom-mode[data-v-8555b560] {
background-color: var(--color-charcoal-400, #313235); background-color: var(--color-charcoal-400, #313235);
color: #fff; color: #fff;
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */ padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
@@ -2159,12 +2159,12 @@ to { transform: rotate(360deg);
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-5514bf46]:focus { .text-input[data-v-8555b560]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-5514bf46] { .clear-button[data-v-8555b560] {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2187,31 +2187,31 @@ to { transform: rotate(360deg);
} }
/* Show clear button when hovering over input wrapper */ /* Show clear button when hovering over input wrapper */
.input-wrapper:hover .clear-button[data-v-5514bf46] { .input-wrapper:hover .clear-button[data-v-8555b560] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-5514bf46]:hover { .clear-button[data-v-8555b560]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-5514bf46] { .clear-button svg[data-v-8555b560] {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46] { .text-input.vue-dom-mode ~ .clear-button[data-v-8555b560] {
right: 8px; right: 8px;
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */ bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
width: 20px; width: 20px;
height: 20px; height: 20px;
background: rgba(107, 114, 128, 0.6); background: rgba(107, 114, 128, 0.6);
} }
.text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-8555b560]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-5514bf46] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-8555b560] {
width: 14px; width: 14px;
height: 14px; height: 14px;
}`)); }`));
@@ -14783,7 +14783,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
modelType: {}, modelType: {},
placeholder: {}, placeholder: {},
showPreview: { type: Boolean }, showPreview: { type: Boolean },
spellcheck: { type: Boolean } spellcheck: { type: Boolean },
maxHeight: {}
}, },
setup(__props) { setup(__props) {
const props = __props; const props = __props;
@@ -14913,10 +14914,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
ref: textareaRef, ref: textareaRef,
placeholder: __props.placeholder, placeholder: __props.placeholder,
spellcheck: __props.spellcheck ?? false, spellcheck: __props.spellcheck ?? false,
class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value }]), class: normalizeClass(["text-input", { "vue-dom-mode": isVueDomMode.value, "lm-wheel-scrollable": isVueDomMode.value }]),
style: normalizeStyle(__props.maxHeight && isVueDomMode.value ? { maxHeight: __props.maxHeight + "px" } : void 0),
"data-capture-wheel": "true",
onInput, onInput,
onWheel onWheel
}, null, 42, _hoisted_3), }, null, 46, _hoisted_3),
showClearButton.value ? (openBlock(), createElementBlock("button", { showClearButton.value ? (openBlock(), createElementBlock("button", {
key: 0, key: 0,
type: "button", type: "button",
@@ -14949,7 +14952,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5514bf46"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-8555b560"]]);
function createVueWidgetCleanup(vueApp, onCleanup) { function createVueWidgetCleanup(vueApp, onCleanup) {
let didUnmount = false; let didUnmount = false;
return () => { return () => {
@@ -15320,6 +15323,8 @@ const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300;
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200; const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200;
const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60; const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60;
const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100; const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100;
const AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT = 400;
const AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT = 300;
const AUTOCOMPLETE_METADATA_VERSION = 1; const AUTOCOMPLETE_METADATA_VERSION = 1;
const LORA_MANAGER_WIDGET_IDS_PROPERTY = "__lm_widget_ids"; const LORA_MANAGER_WIDGET_IDS_PROPERTY = "__lm_widget_ids";
function forwardMiddleMouseToCanvas(container) { function forwardMiddleMouseToCanvas(container) {
@@ -15713,13 +15718,66 @@ function normalizeAutocompleteWidgetValues(node, info) {
info.widgets_values = repairedValues; info.widgets_values = repairedValues;
} }
} }
function applyAutocompleteTextLayoutFix(widget, container, isVueMode) {
if (isVueMode) {
widget.computeLayoutSize = void 0;
widget.computeSize = (width) => [width ?? 200, AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT - 4];
if (container) {
container.style.minHeight = `${AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT}px`;
}
} else {
delete widget.computeLayoutSize;
delete widget.computeSize;
if (container) {
container.style.minHeight = "";
}
}
}
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
var _a2, _b; var _a2, _b;
if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) { if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) {
app$1.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => { app$1.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
var _a3, _b2, _c; var _a3, _b2, _c, _d, _e2, _f;
const isVueDomMode = ((_c = (_b2 = (_a3 = app$1.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false; const isVueDomMode = ((_c = (_b2 = (_a3 = app$1.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false;
if ((_d = app$1.graph) == null ? void 0 : _d.nodes) {
for (const node of app$1.graph.nodes) {
const textWidget = (_e2 = node.widgets) == null ? void 0 : _e2.find(
(w2) => w2.type === "AUTOCOMPLETE_TEXT_LORAS"
);
if (!textWidget) continue;
const container = textWidget.element;
applyAutocompleteTextLayoutFix(textWidget, container, isVueDomMode);
}
}
requestAnimationFrame(() => {
var _a4;
for (const nodeEl of document.querySelectorAll("[data-node-id]")) {
const grid = nodeEl.querySelector('[data-testid="node-widgets"]');
if (!grid) continue;
const nodeId = nodeEl.getAttribute("data-node-id");
const node = (_a4 = app$1.graph) == null ? void 0 : _a4.getNodeById(nodeId);
if (!node) continue;
const rows = [];
let needsFix = false;
for (const w2 of node.widgets ?? []) {
if (w2.type === "LORA_MANAGER_AUTOCOMPLETE_METADATA") {
rows.push("min-content");
} else if (w2.name === "loras") {
rows.push("auto");
} else if (w2.name === "text" && w2.type === "AUTOCOMPLETE_TEXT_LORAS") {
rows.push(isVueDomMode ? "min-content" : "auto");
needsFix = true;
} else {
rows.push("auto");
}
}
if (needsFix) {
grid.style.gridTemplateRows = rows.join(" ");
}
}
});
(_f = app$1.canvas) == null ? void 0 : _f.setDirty(true, true);
document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", { document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", {
detail: { isVueDomMode } detail: { isVueDomMode }
})); }));
@@ -15799,13 +15857,15 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
); );
widget.metadataWidget = metadataWidget; widget.metadataWidget = metadataWidget;
const spellcheck = ((_c = (_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false; const spellcheck = ((_c = (_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
const maxHeight = modelType === "loras" ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : void 0;
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
node, node,
modelType, modelType,
placeholder: inputOptions.placeholder || widgetName, placeholder: inputOptions.placeholder || widgetName,
showPreview: true, showPreview: true,
spellcheck spellcheck,
maxHeight
}); });
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
unstyled: true, unstyled: true,
@@ -15814,10 +15874,23 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
vueApp.mount(container); vueApp.mount(container);
const appKey = instanceId; const appKey = instanceId;
vueApps.set(appKey, vueApp); vueApps.set(appKey, vueApp);
if (maxHeight) {
container.style.maxHeight = `${maxHeight}px`;
container.style.minHeight = `${maxHeight}px`;
}
if (modelType === "loras") {
applyAutocompleteTextLayoutFix(
widget,
container,
typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode
);
}
widget.onRemove = createVueWidgetCleanup(vueApp, () => { widget.onRemove = createVueWidgetCleanup(vueApp, () => {
vueApps.delete(appKey); vueApps.delete(appKey);
}); });
return { widget }; const minWidth = AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT;
const minHeight = modelType === "loras" ? void 0 : AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT;
return { widget, minWidth, minHeight };
} }
app$1.registerExtension({ app$1.registerExtension({
name: "LoraManager.VueWidgets", name: "LoraManager.VueWidgets",

File diff suppressed because one or more lines are too long