mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-24 12:01:16 -03:00
Compare commits
15 Commits
v1.0.11
...
130fb5d2d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130fb5d2d5 | ||
|
|
23c6863a3a | ||
|
|
c0e2578640 | ||
|
|
e3c812367e | ||
|
|
4d239008a6 | ||
|
|
00177a06d0 | ||
|
|
568daa351e | ||
|
|
5a4664fa12 | ||
|
|
dd5b213adc | ||
|
|
d9ee9b3155 | ||
|
|
01dac57c35 | ||
|
|
7f92d09239 | ||
|
|
62f9e3f44a | ||
|
|
e55895786d | ||
|
|
4e3ede23b7 |
@@ -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...",
|
||||||
@@ -1225,7 +1227,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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
@@ -1225,7 +1227,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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
@@ -1225,7 +1227,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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
@@ -1225,7 +1227,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...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "עזרה",
|
"help": "עזרה",
|
||||||
"add": "הוספה",
|
"add": "הוספה",
|
||||||
"close": "סגור",
|
"close": "סגור",
|
||||||
"menu": "תפריט"
|
"menu": "תפריט",
|
||||||
|
"remove": "הסר",
|
||||||
|
"change": "שנה"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "טוען...",
|
"loading": "טוען...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "הערות נשמרו בהצלחה",
|
"saved": "הערות נשמרו בהצלחה",
|
||||||
"saveFailed": "שמירת ההערות נכשלה"
|
"saveFailed": "שמירת ההערות נכשלה",
|
||||||
|
"showMore": "הצג עוד",
|
||||||
|
"showLess": "הצג פחות"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "הוסף פרמטר קבוע מראש...",
|
"addPresetParameter": "הוסף פרמטר קבוע מראש...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "ヘルプ",
|
"help": "ヘルプ",
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"menu": "メニュー"
|
"menu": "メニュー",
|
||||||
|
"remove": "削除",
|
||||||
|
"change": "変更"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "メモが正常に保存されました",
|
"saved": "メモが正常に保存されました",
|
||||||
"saveFailed": "メモの保存に失敗しました"
|
"saveFailed": "メモの保存に失敗しました",
|
||||||
|
"showMore": "もっと見る",
|
||||||
|
"showLess": "折りたたむ"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "プリセットパラメータを追加...",
|
"addPresetParameter": "プリセットパラメータを追加...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "도움말",
|
"help": "도움말",
|
||||||
"add": "추가",
|
"add": "추가",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"menu": "메뉴"
|
"menu": "메뉴",
|
||||||
|
"remove": "제거",
|
||||||
|
"change": "변경"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "메모가 성공적으로 저장됨",
|
"saved": "메모가 성공적으로 저장됨",
|
||||||
"saveFailed": "메모 저장 실패"
|
"saveFailed": "메모 저장 실패",
|
||||||
|
"showMore": "더 보기",
|
||||||
|
"showLess": "접기"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "프리셋 매개변수 추가...",
|
"addPresetParameter": "프리셋 매개변수 추가...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "Справка",
|
"help": "Справка",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"menu": "Меню"
|
"menu": "Меню",
|
||||||
|
"remove": "Удалить",
|
||||||
|
"change": "Изменить"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "Заметки успешно сохранены",
|
"saved": "Заметки успешно сохранены",
|
||||||
"saveFailed": "Не удалось сохранить заметки"
|
"saveFailed": "Не удалось сохранить заметки",
|
||||||
|
"showMore": "Показать больше",
|
||||||
|
"showLess": "Свернуть"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "Добавить предустановленный параметр...",
|
"addPresetParameter": "Добавить предустановленный параметр...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"menu": "菜单"
|
"menu": "菜单",
|
||||||
|
"remove": "移除",
|
||||||
|
"change": "更换"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "备注保存成功",
|
"saved": "备注保存成功",
|
||||||
"saveFailed": "备注保存失败"
|
"saveFailed": "备注保存失败",
|
||||||
|
"showMore": "展开",
|
||||||
|
"showLess": "收起"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "添加预设参数...",
|
"addPresetParameter": "添加预设参数...",
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
"help": "說明",
|
"help": "說明",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"menu": "選單"
|
"menu": "選單",
|
||||||
|
"remove": "移除",
|
||||||
|
"change": "更換"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
@@ -1225,7 +1227,9 @@
|
|||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"saved": "備註已儲存",
|
"saved": "備註已儲存",
|
||||||
"saveFailed": "儲存備註失敗"
|
"saveFailed": "儲存備註失敗",
|
||||||
|
"showMore": "展開",
|
||||||
|
"showLess": "收起"
|
||||||
},
|
},
|
||||||
"usageTips": {
|
"usageTips": {
|
||||||
"addPresetParameter": "新增預設參數...",
|
"addPresetParameter": "新增預設參數...",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
@@ -53,10 +55,7 @@ def resolve_settings_path() -> Path:
|
|||||||
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
||||||
return portable
|
return portable
|
||||||
|
|
||||||
config_home = os.environ.get("XDG_CONFIG_HOME")
|
return Path(user_config_dir(APP_NAME, appauthor=False)) / "settings.json"
|
||||||
if config_home:
|
|
||||||
return Path(config_home).expanduser() / APP_NAME / "settings.json"
|
|
||||||
return Path.home() / ".config" / APP_NAME / "settings.json"
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> dict[str, Any]:
|
def load_json(path: Path) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from platformdirs import user_config_dir
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(message)s",
|
format="%(message)s",
|
||||||
@@ -68,10 +70,7 @@ def resolve_settings_path() -> Path:
|
|||||||
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
||||||
return portable
|
return portable
|
||||||
|
|
||||||
config_home = os.environ.get("XDG_CONFIG_HOME")
|
return Path(user_config_dir(APP_NAME, appauthor=False)) / "settings.json"
|
||||||
if config_home:
|
|
||||||
return Path(config_home).expanduser() / APP_NAME / "settings.json"
|
|
||||||
return Path.home() / ".config" / APP_NAME / "settings.json"
|
|
||||||
|
|
||||||
|
|
||||||
def _load_json(path: Path) -> dict[str, Any]:
|
def _load_json(path: Path) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -140,14 +140,66 @@
|
|||||||
|
|
||||||
/* 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 {
|
||||||
|
|||||||
@@ -669,3 +669,142 @@
|
|||||||
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: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
resize: vertical;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<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.url') }}:</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>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -18,7 +18,18 @@
|
|||||||
</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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -546,6 +546,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 +575,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 +716,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,6 +737,19 @@ 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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
@@ -15713,13 +15716,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 +15855,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,6 +15872,17 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user