mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
Compare commits
4 Commits
5dcfde36ea
...
502b7eab31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502b7eab31 | ||
|
|
be75ad930e | ||
|
|
763c4f4dad | ||
|
|
d32c492bdb |
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"add": "Hinzufügen",
|
"add": "Hinzufügen",
|
||||||
"close": "Schließen"
|
"close": "Schließen",
|
||||||
|
"menu": "Menü"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Wird geladen...",
|
"loading": "Wird geladen...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "Früher Zugriff",
|
"earlyAccess": "Früher Zugriff",
|
||||||
"earlyAccessTooltip": "Für diese Version ist derzeit Civitai Early Access erforderlich",
|
"earlyAccessTooltip": "Für diese Version ist derzeit Civitai Early Access erforderlich",
|
||||||
"ignored": "Ignoriert",
|
"ignored": "Ignoriert",
|
||||||
"ignoredTooltip": "Für diese Version sind Update-Benachrichtigungen deaktiviert"
|
"ignoredTooltip": "Für diese Version sind Update-Benachrichtigungen deaktiviert",
|
||||||
|
"onSiteOnly": "Nur On-Site",
|
||||||
|
"onSiteOnlyTooltip": "Diese Version ist nur für die On-Site-Generierung auf Civitai verfügbar"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
"downloadTooltip": "Diese Version herunterladen",
|
"downloadTooltip": "Diese Version herunterladen",
|
||||||
"downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen",
|
"downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen",
|
||||||
|
"downloadNotAllowedTooltip": "Diese Version ist nur für die On-Site-Generierung auf Civitai verfügbar",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"deleteTooltip": "Diese lokale Version löschen",
|
"deleteTooltip": "Diese lokale Version löschen",
|
||||||
"ignore": "Ignorieren",
|
"ignore": "Ignorieren",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"menu": "Menu"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "Early Access",
|
"earlyAccess": "Early Access",
|
||||||
"earlyAccessTooltip": "This version currently requires Civitai early access",
|
"earlyAccessTooltip": "This version currently requires Civitai early access",
|
||||||
"ignored": "Ignored",
|
"ignored": "Ignored",
|
||||||
"ignoredTooltip": "Update notifications are disabled for this version"
|
"ignoredTooltip": "Update notifications are disabled for this version",
|
||||||
|
"onSiteOnly": "On-Site Only",
|
||||||
|
"onSiteOnlyTooltip": "This version is only available for on-site generation on Civitai"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"downloadTooltip": "Download this version",
|
"downloadTooltip": "Download this version",
|
||||||
"downloadEarlyAccessTooltip": "Download this early access version from Civitai",
|
"downloadEarlyAccessTooltip": "Download this early access version from Civitai",
|
||||||
|
"downloadNotAllowedTooltip": "This version is only available for on-site generation on Civitai",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deleteTooltip": "Delete this local version",
|
"deleteTooltip": "Delete this local version",
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Configuración",
|
"settings": "Configuración",
|
||||||
"help": "Ayuda",
|
"help": "Ayuda",
|
||||||
"add": "Añadir",
|
"add": "Añadir",
|
||||||
"close": "Cerrar"
|
"close": "Cerrar",
|
||||||
|
"menu": "Menú"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "Acceso temprano",
|
"earlyAccess": "Acceso temprano",
|
||||||
"earlyAccessTooltip": "Esta versión requiere actualmente acceso temprano de Civitai",
|
"earlyAccessTooltip": "Esta versión requiere actualmente acceso temprano de Civitai",
|
||||||
"ignored": "Ignorada",
|
"ignored": "Ignorada",
|
||||||
"ignoredTooltip": "Las notificaciones de actualización están desactivadas para esta versión"
|
"ignoredTooltip": "Las notificaciones de actualización están desactivadas para esta versión",
|
||||||
|
"onSiteOnly": "Solo en Sitio",
|
||||||
|
"onSiteOnlyTooltip": "Esta versión solo está disponible para generación en el sitio de Civitai"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
"downloadTooltip": "Descargar esta versión",
|
"downloadTooltip": "Descargar esta versión",
|
||||||
"downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai",
|
"downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai",
|
||||||
|
"downloadNotAllowedTooltip": "Esta versión solo está disponible para generación en el sitio de Civitai",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"deleteTooltip": "Eliminar esta versión local",
|
"deleteTooltip": "Eliminar esta versión local",
|
||||||
"ignore": "Ignorar",
|
"ignore": "Ignorar",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"close": "Fermer"
|
"close": "Fermer",
|
||||||
|
"menu": "Menu"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "Accès anticipé",
|
"earlyAccess": "Accès anticipé",
|
||||||
"earlyAccessTooltip": "Cette version nécessite actuellement l'accès anticipé Civitai",
|
"earlyAccessTooltip": "Cette version nécessite actuellement l'accès anticipé Civitai",
|
||||||
"ignored": "Ignorée",
|
"ignored": "Ignorée",
|
||||||
"ignoredTooltip": "Les notifications de mise à jour sont désactivées pour cette version"
|
"ignoredTooltip": "Les notifications de mise à jour sont désactivées pour cette version",
|
||||||
|
"onSiteOnly": "Uniquement sur Site",
|
||||||
|
"onSiteOnlyTooltip": "Cette version n'est disponible que pour la génération sur le site Civitai"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
"downloadTooltip": "Télécharger cette version",
|
"downloadTooltip": "Télécharger cette version",
|
||||||
"downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai",
|
"downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai",
|
||||||
|
"downloadNotAllowedTooltip": "Cette version n'est disponible que pour la génération sur le site Civitai",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"deleteTooltip": "Supprimer cette version locale",
|
"deleteTooltip": "Supprimer cette version locale",
|
||||||
"ignore": "Ignorer",
|
"ignore": "Ignorer",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "הגדרות",
|
"settings": "הגדרות",
|
||||||
"help": "עזרה",
|
"help": "עזרה",
|
||||||
"add": "הוספה",
|
"add": "הוספה",
|
||||||
"close": "סגור"
|
"close": "סגור",
|
||||||
|
"menu": "תפריט"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "טוען...",
|
"loading": "טוען...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "גישה מוקדמת",
|
"earlyAccess": "גישה מוקדמת",
|
||||||
"earlyAccessTooltip": "גרסה זו דורשת כרגע גישת Early Access של Civitai",
|
"earlyAccessTooltip": "גרסה זו דורשת כרגע גישת Early Access של Civitai",
|
||||||
"ignored": "התעלם",
|
"ignored": "התעלם",
|
||||||
"ignoredTooltip": "התראות העדכון מושבתות עבור גרסה זו"
|
"ignoredTooltip": "התראות העדכון מושבתות עבור גרסה זו",
|
||||||
|
"onSiteOnly": "רק באתר",
|
||||||
|
"onSiteOnlyTooltip": "גרסה זו זמינה רק ליצירה באתר Civitai"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "הורדה",
|
"download": "הורדה",
|
||||||
"downloadTooltip": "הורד את הגרסה הזו",
|
"downloadTooltip": "הורד את הגרסה הזו",
|
||||||
"downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai",
|
"downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai",
|
||||||
|
"downloadNotAllowedTooltip": "גרסה זו זמינה רק ליצירה באתר Civitai",
|
||||||
"delete": "מחיקה",
|
"delete": "מחיקה",
|
||||||
"deleteTooltip": "מחק את הגרסה המקומית הזו",
|
"deleteTooltip": "מחק את הגרסה המקומית הזו",
|
||||||
"ignore": "התעלם",
|
"ignore": "התעלם",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "ヘルプ",
|
"help": "ヘルプ",
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
"close": "閉じる"
|
"close": "閉じる",
|
||||||
|
"menu": "メニュー"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "早期アクセス",
|
"earlyAccess": "早期アクセス",
|
||||||
"earlyAccessTooltip": "このバージョンは現在 Civitai の早期アクセスが必要です",
|
"earlyAccessTooltip": "このバージョンは現在 Civitai の早期アクセスが必要です",
|
||||||
"ignored": "無視中",
|
"ignored": "無視中",
|
||||||
"ignoredTooltip": "このバージョンの更新通知は無効です"
|
"ignoredTooltip": "このバージョンの更新通知は無効です",
|
||||||
|
"onSiteOnly": "サイト内のみ",
|
||||||
|
"onSiteOnlyTooltip": "このバージョンはCivitaiサイト内でのみ利用可能で、ダウンロードはできません"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "ダウンロード",
|
"download": "ダウンロード",
|
||||||
"downloadTooltip": "このバージョンをダウンロード",
|
"downloadTooltip": "このバージョンをダウンロード",
|
||||||
"downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード",
|
"downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード",
|
||||||
|
"downloadNotAllowedTooltip": "このバージョンはCivitaiサイト内でのみ利用可能で、ダウンロードはできません",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"deleteTooltip": "このローカルバージョンを削除",
|
"deleteTooltip": "このローカルバージョンを削除",
|
||||||
"ignore": "無視",
|
"ignore": "無視",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "설정",
|
"settings": "설정",
|
||||||
"help": "도움말",
|
"help": "도움말",
|
||||||
"add": "추가",
|
"add": "추가",
|
||||||
"close": "닫기"
|
"close": "닫기",
|
||||||
|
"menu": "메뉴"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "얼리 액세스",
|
"earlyAccess": "얼리 액세스",
|
||||||
"earlyAccessTooltip": "이 버전은 현재 Civitai 얼리 액세스가 필요합니다",
|
"earlyAccessTooltip": "이 버전은 현재 Civitai 얼리 액세스가 필요합니다",
|
||||||
"ignored": "무시됨",
|
"ignored": "무시됨",
|
||||||
"ignoredTooltip": "이 버전은 업데이트 알림이 비활성화되어 있습니다"
|
"ignoredTooltip": "이 버전은 업데이트 알림이 비활성화되어 있습니다",
|
||||||
|
"onSiteOnly": "사이트 내 전용",
|
||||||
|
"onSiteOnlyTooltip": "이 버전은 Civitai 사이트 내에서만 사용 가능하며 다운로드할 수 없습니다"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "다운로드",
|
"download": "다운로드",
|
||||||
"downloadTooltip": "이 버전 다운로드",
|
"downloadTooltip": "이 버전 다운로드",
|
||||||
"downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드",
|
"downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드",
|
||||||
|
"downloadNotAllowedTooltip": "이 버전은 Civitai 사이트 내에서만 사용 가능하며 다운로드할 수 없습니다",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"deleteTooltip": "이 로컬 버전 삭제",
|
"deleteTooltip": "이 로컬 버전 삭제",
|
||||||
"ignore": "무시",
|
"ignore": "무시",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"help": "Справка",
|
"help": "Справка",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"close": "Закрыть"
|
"close": "Закрыть",
|
||||||
|
"menu": "Меню"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "Ранний доступ",
|
"earlyAccess": "Ранний доступ",
|
||||||
"earlyAccessTooltip": "Для этой версии сейчас требуется ранний доступ Civitai",
|
"earlyAccessTooltip": "Для этой версии сейчас требуется ранний доступ Civitai",
|
||||||
"ignored": "Игнорируется",
|
"ignored": "Игнорируется",
|
||||||
"ignoredTooltip": "Уведомления об обновлениях для этой версии отключены"
|
"ignoredTooltip": "Уведомления об обновлениях для этой версии отключены",
|
||||||
|
"onSiteOnly": "Только на Сайте",
|
||||||
|
"onSiteOnlyTooltip": "Эта версия доступна только для генерации на сайте Civitai"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "Скачать",
|
"download": "Скачать",
|
||||||
"downloadTooltip": "Скачать эту версию",
|
"downloadTooltip": "Скачать эту версию",
|
||||||
"downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai",
|
"downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai",
|
||||||
|
"downloadNotAllowedTooltip": "Эта версия доступна только для генерации на сайте Civitai",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"deleteTooltip": "Удалить эту локальную версию",
|
"deleteTooltip": "Удалить эту локальную версию",
|
||||||
"ignore": "Игнорировать",
|
"ignore": "Игнорировать",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"menu": "菜单"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "抢先体验",
|
"earlyAccess": "抢先体验",
|
||||||
"earlyAccessTooltip": "此版本当前需要 Civitai 抢先体验权限",
|
"earlyAccessTooltip": "此版本当前需要 Civitai 抢先体验权限",
|
||||||
"ignored": "已忽略",
|
"ignored": "已忽略",
|
||||||
"ignoredTooltip": "此版本已关闭更新通知"
|
"ignoredTooltip": "此版本已关闭更新通知",
|
||||||
|
"onSiteOnly": "仅站内生成",
|
||||||
|
"onSiteOnlyTooltip": "此版本仅在 Civitai 站内可用,无法下载"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
"downloadTooltip": "下载此版本",
|
"downloadTooltip": "下载此版本",
|
||||||
"downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本",
|
"downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本",
|
||||||
|
"downloadNotAllowedTooltip": "此版本仅在 Civitai 站内可用,无法下载",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"deleteTooltip": "删除此本地版本",
|
"deleteTooltip": "删除此本地版本",
|
||||||
"ignore": "忽略",
|
"ignore": "忽略",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "說明",
|
"help": "說明",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"close": "關閉"
|
"close": "關閉",
|
||||||
|
"menu": "選單"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
@@ -1292,12 +1293,15 @@
|
|||||||
"earlyAccess": "搶先體驗",
|
"earlyAccess": "搶先體驗",
|
||||||
"earlyAccessTooltip": "此版本目前需要 Civitai 搶先體驗權限",
|
"earlyAccessTooltip": "此版本目前需要 Civitai 搶先體驗權限",
|
||||||
"ignored": "已忽略",
|
"ignored": "已忽略",
|
||||||
"ignoredTooltip": "此版本已關閉更新通知"
|
"ignoredTooltip": "此版本已關閉更新通知",
|
||||||
|
"onSiteOnly": "僅站內生成",
|
||||||
|
"onSiteOnlyTooltip": "此版本僅在 Civitai 站內可用,無法下載"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"download": "下載",
|
"download": "下載",
|
||||||
"downloadTooltip": "下載此版本",
|
"downloadTooltip": "下載此版本",
|
||||||
"downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本",
|
"downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本",
|
||||||
|
"downloadNotAllowedTooltip": "此版本僅在 Civitai 站內可用,無法下載",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"deleteTooltip": "刪除此本地版本",
|
"deleteTooltip": "刪除此本地版本",
|
||||||
"ignore": "忽略",
|
"ignore": "忽略",
|
||||||
|
|||||||
@@ -2423,6 +2423,7 @@ class ModelUpdateHandler:
|
|||||||
"shouldIgnore": version.should_ignore,
|
"shouldIgnore": version.should_ignore,
|
||||||
"earlyAccessEndsAt": version.early_access_ends_at,
|
"earlyAccessEndsAt": version.early_access_ends_at,
|
||||||
"isEarlyAccess": is_early_access,
|
"isEarlyAccess": is_early_access,
|
||||||
|
"usageControl": version.usage_control,
|
||||||
"filePath": context.get("file_path"),
|
"filePath": context.get("file_path"),
|
||||||
"fileName": context.get("file_name"),
|
"fileName": context.get("file_name"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class ModelVersionRecord:
|
|||||||
early_access_ends_at: Optional[str] = None
|
early_access_ends_at: Optional[str] = None
|
||||||
sort_index: int = 0
|
sort_index: int = 0
|
||||||
is_early_access: bool = False
|
is_early_access: bool = False
|
||||||
|
usage_control: Optional[str] = None # "Download", "Generation", "InternalGeneration"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -101,11 +102,14 @@ class ModelUpdateRecord:
|
|||||||
|
|
||||||
return [version.version_id for version in self.versions if version.is_in_library]
|
return [version.version_id for version in self.versions if version.is_in_library]
|
||||||
|
|
||||||
def has_update(self, hide_early_access: bool = False) -> bool:
|
def has_update(
|
||||||
|
self, hide_early_access: bool = False, hide_non_downloadable: bool = True
|
||||||
|
) -> bool:
|
||||||
"""Return True when a non-ignored remote version newer than the newest local copy is available.
|
"""Return True when a non-ignored remote version newer than the newest local copy is available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
hide_early_access: If True, exclude early access versions from update check.
|
hide_early_access: If True, exclude early access versions from update check.
|
||||||
|
hide_non_downloadable: If True, exclude versions that don't allow downloads.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.should_ignore_model:
|
if self.should_ignore_model:
|
||||||
@@ -121,6 +125,7 @@ class ModelUpdateRecord:
|
|||||||
not version.is_in_library
|
not version.is_in_library
|
||||||
and not version.should_ignore
|
and not version.should_ignore
|
||||||
and not (hide_early_access and ModelUpdateRecord._is_early_access_active(version))
|
and not (hide_early_access and ModelUpdateRecord._is_early_access_active(version))
|
||||||
|
and not (hide_non_downloadable and not ModelUpdateRecord._is_downloadable(version))
|
||||||
for version in self.versions
|
for version in self.versions
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,6 +134,8 @@ class ModelUpdateRecord:
|
|||||||
continue
|
continue
|
||||||
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
||||||
continue
|
continue
|
||||||
|
if hide_non_downloadable and not ModelUpdateRecord._is_downloadable(version):
|
||||||
|
continue
|
||||||
if version.version_id > max_in_library:
|
if version.version_id > max_in_library:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -155,11 +162,18 @@ class ModelUpdateRecord:
|
|||||||
# Phase 1: Basic EA flag from bulk API
|
# Phase 1: Basic EA flag from bulk API
|
||||||
return version.is_early_access
|
return version.is_early_access
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_downloadable(version: ModelVersionRecord) -> bool:
|
||||||
|
if version.usage_control is None:
|
||||||
|
return True
|
||||||
|
return version.usage_control == "Download"
|
||||||
|
|
||||||
def has_update_for_base(
|
def has_update_for_base(
|
||||||
self,
|
self,
|
||||||
local_version_id: Optional[int],
|
local_version_id: Optional[int],
|
||||||
local_base_model: Optional[str],
|
local_base_model: Optional[str],
|
||||||
hide_early_access: bool = False,
|
hide_early_access: bool = False,
|
||||||
|
hide_non_downloadable: bool = True,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return True when a newer remote version with the same base model exists.
|
"""Return True when a newer remote version with the same base model exists.
|
||||||
|
|
||||||
@@ -167,6 +181,7 @@ class ModelUpdateRecord:
|
|||||||
local_version_id: The current local version id.
|
local_version_id: The current local version id.
|
||||||
local_base_model: The base model to filter by.
|
local_base_model: The base model to filter by.
|
||||||
hide_early_access: If True, exclude early access versions from update check.
|
hide_early_access: If True, exclude early access versions from update check.
|
||||||
|
hide_non_downloadable: If True, exclude versions that don't allow downloads.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.should_ignore_model:
|
if self.should_ignore_model:
|
||||||
@@ -197,6 +212,8 @@ class ModelUpdateRecord:
|
|||||||
continue
|
continue
|
||||||
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
||||||
continue
|
continue
|
||||||
|
if hide_non_downloadable and not ModelUpdateRecord._is_downloadable(version):
|
||||||
|
continue
|
||||||
version_base = _normalize_base_model(version.base_model)
|
version_base = _normalize_base_model(version.base_model)
|
||||||
if version_base != normalized_base:
|
if version_base != normalized_base:
|
||||||
continue
|
continue
|
||||||
@@ -230,6 +247,7 @@ class ModelUpdateService:
|
|||||||
preview_url TEXT,
|
preview_url TEXT,
|
||||||
is_in_library INTEGER NOT NULL DEFAULT 0,
|
is_in_library INTEGER NOT NULL DEFAULT 0,
|
||||||
should_ignore INTEGER NOT NULL DEFAULT 0,
|
should_ignore INTEGER NOT NULL DEFAULT 0,
|
||||||
|
usage_control TEXT,
|
||||||
PRIMARY KEY (model_id, version_id),
|
PRIMARY KEY (model_id, version_id),
|
||||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -465,6 +483,10 @@ class ModelUpdateService:
|
|||||||
"ALTER TABLE model_update_versions "
|
"ALTER TABLE model_update_versions "
|
||||||
"ADD COLUMN is_early_access INTEGER NOT NULL DEFAULT 0"
|
"ADD COLUMN is_early_access INTEGER NOT NULL DEFAULT 0"
|
||||||
),
|
),
|
||||||
|
"usage_control": (
|
||||||
|
"ALTER TABLE model_update_versions "
|
||||||
|
"ADD COLUMN usage_control TEXT"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
for column, statement in migrations.items():
|
for column, statement in migrations.items():
|
||||||
@@ -1337,6 +1359,7 @@ class ModelUpdateService:
|
|||||||
# Check availability field from bulk API for basic EA detection
|
# Check availability field from bulk API for basic EA detection
|
||||||
availability = _normalize_string(entry.get("availability"))
|
availability = _normalize_string(entry.get("availability"))
|
||||||
is_early_access = availability == "EarlyAccess"
|
is_early_access = availability == "EarlyAccess"
|
||||||
|
usage_control = _normalize_string(entry.get("usageControl"))
|
||||||
|
|
||||||
return ModelVersionRecord(
|
return ModelVersionRecord(
|
||||||
version_id=version_id,
|
version_id=version_id,
|
||||||
@@ -1350,6 +1373,7 @@ class ModelUpdateService:
|
|||||||
early_access_ends_at=early_access_ends_at,
|
early_access_ends_at=early_access_ends_at,
|
||||||
sort_index=index,
|
sort_index=index,
|
||||||
is_early_access=is_early_access,
|
is_early_access=is_early_access,
|
||||||
|
usage_control=usage_control,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _extract_size_bytes(self, files) -> Optional[int]:
|
def _extract_size_bytes(self, files) -> Optional[int]:
|
||||||
@@ -1464,7 +1488,7 @@ class ModelUpdateService:
|
|||||||
f"""
|
f"""
|
||||||
SELECT model_id, version_id, sort_index, name, base_model, released_at,
|
SELECT model_id, version_id, sort_index, name, base_model, released_at,
|
||||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||||
is_early_access
|
is_early_access, usage_control
|
||||||
FROM model_update_versions
|
FROM model_update_versions
|
||||||
WHERE model_id IN ({placeholders})
|
WHERE model_id IN ({placeholders})
|
||||||
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
||||||
@@ -1492,6 +1516,7 @@ class ModelUpdateService:
|
|||||||
early_access_ends_at=row["early_access_ends_at"],
|
early_access_ends_at=row["early_access_ends_at"],
|
||||||
sort_index=_normalize_int(row["sort_index"]) or 0,
|
sort_index=_normalize_int(row["sort_index"]) or 0,
|
||||||
is_early_access=bool(row["is_early_access"]),
|
is_early_access=bool(row["is_early_access"]),
|
||||||
|
usage_control=row["usage_control"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1548,8 +1573,8 @@ class ModelUpdateService:
|
|||||||
INSERT INTO model_update_versions (
|
INSERT INTO model_update_versions (
|
||||||
version_id, model_id, sort_index, name, base_model, released_at,
|
version_id, model_id, sort_index, name, base_model, released_at,
|
||||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||||
is_early_access
|
is_early_access, usage_control
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
version.version_id,
|
version.version_id,
|
||||||
@@ -1564,6 +1589,7 @@ class ModelUpdateService:
|
|||||||
1 if version.should_ignore else 0,
|
1 if version.should_ignore else 0,
|
||||||
version.early_access_ends_at,
|
version.early_access_ends_at,
|
||||||
1 if version.is_early_access else 0,
|
1 if version.is_early_access else 0,
|
||||||
|
version.usage_control,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
354
scripts/migrate_legacy_metadata.py
Normal file
354
scripts/migrate_legacy_metadata.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migrate metadata from old sidecar JSON format to LoRA Manager's metadata.json format.
|
||||||
|
|
||||||
|
This script automatically discovers model folders from LoRA Manager's settings.json,
|
||||||
|
finds JSON files with the same basename as model files (e.g., `model.json` for
|
||||||
|
`model.safetensors`), and migrates their content to the corresponding `.metadata.json` files.
|
||||||
|
|
||||||
|
Fields migrated:
|
||||||
|
- "activation text" → civitai.trainedWords (array of trigger words)
|
||||||
|
- "preferred weight" → usage_tips.strength (LoRA only, skipped for Checkpoint)
|
||||||
|
- "notes" → notes (user-defined notes)
|
||||||
|
|
||||||
|
Supported model types: LoRA, Checkpoint
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/migrate_legacy_metadata.py [--dry-run] [--verbose]
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
1. Read settings.json to find all configured model folders
|
||||||
|
2. Recursively scan for model files (.safetensors, .ckpt, .pt, .pth, .bin)
|
||||||
|
3. Find corresponding legacy metadata JSON files
|
||||||
|
4. Migrate data to .metadata.json files
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
APP_NAME = "ComfyUI-LoRA-Manager"
|
||||||
|
MODEL_EXTENSIONS = {".safetensors", ".ckpt", ".pt", ".pth", ".bin"}
|
||||||
|
SECRET_PATTERN = re.compile(r"(key|token|secret|password|auth|credential)", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_settings_path() -> Path:
|
||||||
|
repo_root = Path(__file__).parent.parent.resolve()
|
||||||
|
portable = repo_root / "settings.json"
|
||||||
|
if portable.exists():
|
||||||
|
payload = load_json(portable)
|
||||||
|
if isinstance(payload, dict) and payload.get("use_portable_settings") is True:
|
||||||
|
return portable
|
||||||
|
|
||||||
|
config_home = os.environ.get("XDG_CONFIG_HOME")
|
||||||
|
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]:
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
logger.error(f"Invalid JSON in {path}: {exc}")
|
||||||
|
return {}
|
||||||
|
except OSError as exc:
|
||||||
|
logger.error(f"Cannot read {path}: {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def expand_path(value: str) -> str:
|
||||||
|
return str(Path(value).expanduser().resolve(strict=False))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path_list(value: Any) -> list[str]:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [expand_path(value)] if value else []
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [expand_path(item) for item in value if isinstance(item, str) and item]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def dedupe(values: list[str]) -> list[str]:
|
||||||
|
seen: set[str] = set()
|
||||||
|
result: list[str] = []
|
||||||
|
for value in values:
|
||||||
|
if value not in seen:
|
||||||
|
result.append(value)
|
||||||
|
seen.add(value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_roots(settings: dict[str, Any]) -> dict[str, list[str]]:
|
||||||
|
roots: dict[str, list[str]] = {}
|
||||||
|
active_library = settings.get("active_library") or "default"
|
||||||
|
sources = [settings]
|
||||||
|
library = settings.get("libraries", {}).get(active_library)
|
||||||
|
if isinstance(library, dict):
|
||||||
|
sources.insert(0, library)
|
||||||
|
for source in sources:
|
||||||
|
folder_paths = source.get("folder_paths")
|
||||||
|
if isinstance(folder_paths, dict):
|
||||||
|
for key, value in folder_paths.items():
|
||||||
|
roots.setdefault(key, []).extend(normalize_path_list(value))
|
||||||
|
for default_key, folder_key in (
|
||||||
|
("default_lora_root", "loras"),
|
||||||
|
("default_checkpoint_root", "checkpoints"),
|
||||||
|
("default_embedding_root", "embeddings"),
|
||||||
|
("default_unet_root", "unet"),
|
||||||
|
):
|
||||||
|
value = settings.get(default_key)
|
||||||
|
if isinstance(value, str) and value:
|
||||||
|
roots.setdefault(folder_key, []).append(expand_path(value))
|
||||||
|
return {key: dedupe(values) for key, values in roots.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def find_model_files(directory: Path) -> list[Path]:
|
||||||
|
model_files = []
|
||||||
|
for ext in MODEL_EXTENSIONS:
|
||||||
|
model_files.extend(directory.rglob(f"*{ext}"))
|
||||||
|
return model_files
|
||||||
|
|
||||||
|
|
||||||
|
def find_legacy_metadata(model_path: Path) -> Path | None:
|
||||||
|
base_name = model_path.stem
|
||||||
|
legacy_path = model_path.with_name(f"{base_name}.json")
|
||||||
|
if legacy_path.exists() and legacy_path.is_file():
|
||||||
|
return legacy_path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_legacy_metadata(legacy_path: Path) -> dict[str, Any] | None:
|
||||||
|
try:
|
||||||
|
with open(legacy_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Invalid JSON in legacy file {legacy_path}: {e}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading legacy file {legacy_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_metadata(metadata_path: Path) -> dict[str, Any]:
|
||||||
|
if not metadata_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(metadata_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning(f"Invalid JSON in metadata file {metadata_path}: {e}. Starting fresh.")
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading metadata file {metadata_path}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def save_metadata(metadata_path: Path, data: dict[str, Any], dry_run: bool = False) -> bool:
|
||||||
|
if dry_run:
|
||||||
|
logger.info(f"[DRY RUN] Would save metadata to: {metadata_path}")
|
||||||
|
return True
|
||||||
|
temp_path = metadata_path.with_suffix(".tmp")
|
||||||
|
try:
|
||||||
|
with open(temp_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
os.replace(temp_path, metadata_path)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving metadata to {metadata_path}: {e}")
|
||||||
|
if temp_path.exists():
|
||||||
|
try:
|
||||||
|
temp_path.unlink()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_metadata(
|
||||||
|
legacy_data: dict[str, Any],
|
||||||
|
existing_metadata: dict[str, Any],
|
||||||
|
model_type: str
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
metadata = existing_metadata.copy()
|
||||||
|
changes_made = False
|
||||||
|
if "civitai" not in metadata:
|
||||||
|
metadata["civitai"] = {}
|
||||||
|
activation_text = legacy_data.get("activation text")
|
||||||
|
if activation_text and isinstance(activation_text, str):
|
||||||
|
trigger_words = [
|
||||||
|
word.strip()
|
||||||
|
for word in activation_text.replace("\n", ",").split(",")
|
||||||
|
if word.strip()
|
||||||
|
]
|
||||||
|
if trigger_words:
|
||||||
|
existing_trained = metadata["civitai"].get("trainedWords", [])
|
||||||
|
if not isinstance(existing_trained, list):
|
||||||
|
existing_trained = []
|
||||||
|
merged = list(dict.fromkeys(existing_trained + trigger_words))
|
||||||
|
if merged != existing_trained:
|
||||||
|
metadata["civitai"]["trainedWords"] = merged
|
||||||
|
changes_made = True
|
||||||
|
logger.debug(f" Migrated activation text: {trigger_words}")
|
||||||
|
if model_type == "lora":
|
||||||
|
preferred_weight = legacy_data.get("preferred weight")
|
||||||
|
if preferred_weight is not None:
|
||||||
|
try:
|
||||||
|
weight_value = float(preferred_weight)
|
||||||
|
usage_tips_str = metadata.get("usage_tips", "{}")
|
||||||
|
if isinstance(usage_tips_str, str):
|
||||||
|
try:
|
||||||
|
usage_tips = json.loads(usage_tips_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
usage_tips = {}
|
||||||
|
elif isinstance(usage_tips_str, dict):
|
||||||
|
usage_tips = usage_tips_str
|
||||||
|
else:
|
||||||
|
usage_tips = {}
|
||||||
|
if "strength" not in usage_tips:
|
||||||
|
usage_tips["strength"] = weight_value
|
||||||
|
metadata["usage_tips"] = json.dumps(usage_tips, ensure_ascii=False)
|
||||||
|
changes_made = True
|
||||||
|
logger.debug(f" Migrated preferred weight: {weight_value}")
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
logger.warning(f" Could not parse preferred weight '{preferred_weight}': {e}")
|
||||||
|
else:
|
||||||
|
if legacy_data.get("preferred weight") is not None:
|
||||||
|
logger.debug(" Skipping 'preferred weight' for non-LoRA model")
|
||||||
|
notes = legacy_data.get("notes")
|
||||||
|
if notes and isinstance(notes, str) and notes.strip():
|
||||||
|
existing_notes = metadata.get("notes", "")
|
||||||
|
if not existing_notes:
|
||||||
|
metadata["notes"] = notes.strip()
|
||||||
|
changes_made = True
|
||||||
|
logger.debug(" Migrated notes")
|
||||||
|
elif notes.strip() not in existing_notes:
|
||||||
|
metadata["notes"] = f"{existing_notes}\n\n{notes.strip()}".strip()
|
||||||
|
changes_made = True
|
||||||
|
logger.debug(" Appended notes")
|
||||||
|
return metadata if changes_made else None
|
||||||
|
|
||||||
|
|
||||||
|
def process_model(model_path: Path, model_type: str, dry_run: bool = False) -> bool:
|
||||||
|
legacy_path = find_legacy_metadata(model_path)
|
||||||
|
if not legacy_path:
|
||||||
|
return True
|
||||||
|
logger.info(f"Processing: {model_path.name} ({model_type})")
|
||||||
|
logger.info(f" Found legacy metadata: {legacy_path.name}")
|
||||||
|
legacy_data = load_legacy_metadata(legacy_path)
|
||||||
|
if legacy_data is None:
|
||||||
|
return False
|
||||||
|
metadata_path = model_path.with_suffix(".metadata.json")
|
||||||
|
existing_metadata = load_metadata(metadata_path)
|
||||||
|
migrated = migrate_metadata(legacy_data, existing_metadata, model_type)
|
||||||
|
if migrated is None:
|
||||||
|
logger.info(" No changes needed (fields already exist or no migratable data)")
|
||||||
|
return True
|
||||||
|
if save_metadata(metadata_path, migrated, dry_run):
|
||||||
|
logger.info(f" ✓ Successfully migrated metadata to: {metadata_path.name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(" ✗ Failed to save metadata")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Migrate legacy metadata JSON files to LoRA Manager's metadata.json format.",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
python scripts/migrate_legacy_metadata.py
|
||||||
|
python scripts/migrate_legacy_metadata.py --dry-run
|
||||||
|
python scripts/migrate_legacy_metadata.py --verbose
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Preview changes without modifying any files"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable verbose output"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.verbose:
|
||||||
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
settings_path = resolve_settings_path()
|
||||||
|
logger.info(f"Using settings: {settings_path}")
|
||||||
|
settings = load_json(settings_path)
|
||||||
|
if not settings:
|
||||||
|
logger.error("Could not load settings.json. Please ensure LoRA Manager is configured.")
|
||||||
|
return 1
|
||||||
|
roots = get_model_roots(settings)
|
||||||
|
if not roots:
|
||||||
|
logger.error("No model folders configured in settings.json.")
|
||||||
|
return 1
|
||||||
|
lora_roots = roots.get("loras", [])
|
||||||
|
checkpoint_roots = roots.get("checkpoints", []) + roots.get("unet", [])
|
||||||
|
all_roots = []
|
||||||
|
for root_list in [lora_roots, checkpoint_roots]:
|
||||||
|
for root in root_list:
|
||||||
|
path = Path(root)
|
||||||
|
if path.exists() and path.is_dir():
|
||||||
|
all_roots.append((path, "lora" if root in lora_roots else "checkpoint"))
|
||||||
|
if not all_roots:
|
||||||
|
logger.error("No valid model folders found.")
|
||||||
|
return 1
|
||||||
|
logger.info(f"Found {len(lora_roots)} LoRA root(s), {len(checkpoint_roots)} Checkpoint root(s)")
|
||||||
|
processed = 0
|
||||||
|
migrated = 0
|
||||||
|
errors = 0
|
||||||
|
skipped = 0
|
||||||
|
lora_count = 0
|
||||||
|
checkpoint_count = 0
|
||||||
|
for root_path, model_type in all_roots:
|
||||||
|
logger.info(f"Scanning: {root_path} ({model_type})")
|
||||||
|
model_files = find_model_files(root_path)
|
||||||
|
logger.debug(f" Found {len(model_files)} model files")
|
||||||
|
for model_path in model_files:
|
||||||
|
legacy_path = find_legacy_metadata(model_path)
|
||||||
|
if not legacy_path:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
processed += 1
|
||||||
|
if process_model(model_path, model_type, dry_run=args.dry_run):
|
||||||
|
migrated += 1
|
||||||
|
if model_type == "lora":
|
||||||
|
lora_count += 1
|
||||||
|
else:
|
||||||
|
checkpoint_count += 1
|
||||||
|
else:
|
||||||
|
errors += 1
|
||||||
|
logger.info("\n" + "=" * 50)
|
||||||
|
logger.info("Migration Summary:")
|
||||||
|
logger.info(f" Models with legacy metadata: {processed}")
|
||||||
|
logger.info(f" Successfully migrated: {migrated}")
|
||||||
|
logger.info(f" - LoRA models: {lora_count}")
|
||||||
|
logger.info(f" - Checkpoint models: {checkpoint_count}")
|
||||||
|
logger.info(f" Errors: {errors}")
|
||||||
|
logger.info(f" Skipped (no legacy file): {skipped}")
|
||||||
|
if args.dry_run:
|
||||||
|
logger.info("\n [DRY RUN MODE - No files were modified]")
|
||||||
|
return 0 if errors == 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
transition: transform 160ms ease-out;
|
transition: transform 160ms 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 */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -370,7 +371,16 @@
|
|||||||
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
@media (max-width: 1200px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card {
|
||||||
|
max-width: 240px;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.card-grid {
|
.card-grid {
|
||||||
grid-template-columns: minmax(260px, 1fr); /* Adjusted minimum size for mobile */
|
grid-template-columns: minmax(260px, 1fr); /* Adjusted minimum size for mobile */
|
||||||
@@ -378,6 +388,7 @@
|
|||||||
|
|
||||||
.model-card {
|
.model-card {
|
||||||
max-width: 100%; /* Allow cards to fill available space on mobile */
|
max-width: 100%; /* Allow cards to fill available space on mobile */
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,8 +574,13 @@ body.hide-card-version .civitai-version {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 160ms ease-out;
|
transition: transform 160ms ease-out;
|
||||||
margin: 0; /* Remove margins, positioning is handled by VirtualScroller */
|
margin: 0;
|
||||||
width: 100%; /* Allow width to be set by the VirtualScroller */
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow cards to grow beyond 260px in virtual scroll mode */
|
||||||
|
.virtual-scroll-item.model-card {
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtual-scroll-item:hover {
|
.virtual-scroll-item:hover {
|
||||||
@@ -576,11 +592,11 @@ body.hide-card-version .civitai-version {
|
|||||||
.card-grid.virtual-scroll {
|
.card-grid.virtual-scroll {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 auto;
|
margin: 0; /* Remove auto margins - positioning handled by VirtualScroller leftOffset */
|
||||||
padding: 4px 0; /* Add top/bottom padding equivalent to card padding */
|
padding: 4px 0; /* Add top/bottom padding equivalent to card padding */
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1400px; /* Keep the max-width from original grid */
|
max-width: none; /* Remove max-width constraint - handled by VirtualScroller */
|
||||||
box-sizing: border-box; /* Include padding in width calculation */
|
box-sizing: border-box; /* Include padding in width calculation */
|
||||||
overflow-x: hidden; /* Prevent horizontal overflow */
|
overflow-x: hidden; /* Prevent horizontal overflow */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,22 @@
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Left section: Logo + Navigation */
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right section: Controls */
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive header container for larger screens */
|
/* Responsive header container for larger screens */
|
||||||
@media (min-width: 2150px) {
|
@media (min-width: 2150px) {
|
||||||
.header-container {
|
.header-container {
|
||||||
@@ -77,6 +93,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item:hover,
|
.nav-item:hover,
|
||||||
@@ -97,13 +114,100 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header search */
|
/* Header search - Centered with VS Code command palette style */
|
||||||
.header-search {
|
.header-search {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 400px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* VS Code command palette style search container */
|
||||||
|
.header-search .search-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--input-bg, var(--card-bg));
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm, 6px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-container:focus-within {
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--lora-accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
padding-left: 2.25rem !important;
|
||||||
|
padding-right: 5rem !important;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-options-toggle,
|
||||||
|
.header-search .search-filter-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius-xs, 4px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-options-toggle {
|
||||||
|
right: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-options-toggle:hover,
|
||||||
|
.header-search .search-filter-toggle:hover {
|
||||||
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .filter-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Disabled state for header search */
|
/* Disabled state for header search */
|
||||||
.header-search.disabled {
|
.header-search.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -247,44 +351,207 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile adjustments */
|
/* Hamburger menu button - hidden by default */
|
||||||
@media (max-width: 768px) {
|
.hamburger-menu-btn {
|
||||||
.app-title {
|
|
||||||
display: none;
|
display: none;
|
||||||
/* Hide text title on mobile */
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu-btn:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hamburger dropdown menu */
|
||||||
|
.hamburger-dropdown {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm, 6px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: 0.5rem;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: var(--z-dropdown, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown .dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: var(--border-radius-xs, 4px);
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown .dropdown-item:hover {
|
||||||
|
background: var(--lora-surface-hover, oklch(95% 0.02 256));
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown .dropdown-item i {
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown .dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color);
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: Early optimization at 1200px - reduce gaps and padding */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.header-container {
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-nav {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls {
|
.header-controls {
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls>div {
|
.header-controls > div {
|
||||||
width: 28px;
|
width: 30px;
|
||||||
height: 28px;
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: Hide nav icons at 1100px to save space */
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.nav-item {
|
||||||
|
gap: 0;
|
||||||
|
padding: 0.25rem 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search {
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
.app-title {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu-btn {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-dropdown.active {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search {
|
.header-search {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0 0.5rem;
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
margin-right: 0.5rem;
|
gap: 0.25rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 0.25rem 0.35rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For very small screens */
|
/* Responsive: Compact mode at 768px */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-search input {
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 4.5rem !important;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search .search-container {
|
||||||
|
border-radius: var(--border-radius-xs, 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For very small screens - switch nav to icons only */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.header-container {
|
.header-container {
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-nav {
|
.main-nav {
|
||||||
display: none;
|
display: flex;
|
||||||
/* Hide navigation on very small screens */
|
gap: 0.15rem;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-search {
|
.nav-item {
|
||||||
flex: 1;
|
padding: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i {
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Position relative for hamburger menu positioning */
|
||||||
|
.header-right {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|||||||
@@ -374,6 +374,14 @@
|
|||||||
background: color-mix(in oklch, var(--lora-surface) 35%, transparent);
|
background: color-mix(in oklch, var(--lora-surface) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-action-disabled {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.version-action:disabled {
|
.version-action:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -271,11 +271,16 @@
|
|||||||
|
|
||||||
/* Enhanced Sidebar Breadcrumb Styles */
|
/* Enhanced Sidebar Breadcrumb Styles */
|
||||||
.sidebar-breadcrumb-container {
|
.sidebar-breadcrumb-container {
|
||||||
margin-top: 8px;
|
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border-radius: var(--border-radius-xs);
|
border-radius: var(--border-radius-xs);
|
||||||
|
/* Sticky positioning to stick below header when scrolling
|
||||||
|
top: 0 means stick at the top of the scroll container (page-content)
|
||||||
|
which is at header height (48px) from the viewport */
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: calc(var(--z-header) - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-breadcrumb-nav {
|
.sidebar-breadcrumb-nav {
|
||||||
@@ -284,7 +289,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
padding: 0 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-breadcrumb-item {
|
.sidebar-breadcrumb-item {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
top: -54px;
|
top: -54px;
|
||||||
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-2) 0;
|
padding: var(--space-1) 0;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +397,33 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Intermediate breakpoint: wrap controls-right to prevent overflow */
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
.actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce button sizes to fit better */
|
||||||
|
.control-group button {
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.actions {
|
.actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -129,6 +129,126 @@ export class HeaderManager {
|
|||||||
|
|
||||||
// Hide search functionality on Statistics page
|
// Hide search functionality on Statistics page
|
||||||
this.updateHeaderForPage();
|
this.updateHeaderForPage();
|
||||||
|
|
||||||
|
// Initialize hamburger menu for mobile
|
||||||
|
this.initializeHamburgerMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeHamburgerMenu() {
|
||||||
|
const hamburgerBtn = document.getElementById('hamburgerMenuBtn');
|
||||||
|
const hamburgerDropdown = document.getElementById('hamburgerDropdown');
|
||||||
|
|
||||||
|
if (!hamburgerBtn || !hamburgerDropdown) return;
|
||||||
|
|
||||||
|
// Toggle dropdown on hamburger button click
|
||||||
|
hamburgerBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
hamburgerDropdown.classList.toggle('active');
|
||||||
|
const icon = hamburgerBtn.querySelector('i');
|
||||||
|
if (hamburgerDropdown.classList.contains('active')) {
|
||||||
|
icon.classList.remove('fa-bars');
|
||||||
|
icon.classList.add('fa-times');
|
||||||
|
} else {
|
||||||
|
icon.classList.remove('fa-times');
|
||||||
|
icon.classList.add('fa-bars');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle dropdown item clicks
|
||||||
|
const dropdownItems = hamburgerDropdown.querySelectorAll('.dropdown-item');
|
||||||
|
dropdownItems.forEach(item => {
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
const action = item.dataset.action;
|
||||||
|
this.handleHamburgerAction(action);
|
||||||
|
hamburgerDropdown.classList.remove('active');
|
||||||
|
const icon = hamburgerBtn.querySelector('i');
|
||||||
|
icon.classList.remove('fa-times');
|
||||||
|
icon.classList.add('fa-bars');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!hamburgerDropdown.contains(e.target) && !hamburgerBtn.contains(e.target)) {
|
||||||
|
hamburgerDropdown.classList.remove('active');
|
||||||
|
const icon = hamburgerBtn.querySelector('i');
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.remove('fa-times');
|
||||||
|
icon.classList.add('fa-bars');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update theme icon in hamburger menu based on current theme
|
||||||
|
this.updateHamburgerThemeIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHamburgerAction(action) {
|
||||||
|
switch (action) {
|
||||||
|
case 'theme':
|
||||||
|
if (typeof toggleTheme === 'function') {
|
||||||
|
const newTheme = toggleTheme();
|
||||||
|
// Update theme toggle in header if it exists
|
||||||
|
const themeToggle = document.querySelector('.theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
|
||||||
|
themeToggle.classList.add(`theme-${newTheme}`);
|
||||||
|
this.updateThemeTooltip(themeToggle, newTheme);
|
||||||
|
}
|
||||||
|
this.updateHamburgerThemeIcon();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'settings':
|
||||||
|
if (window.settingsManager) {
|
||||||
|
window.settingsManager.toggleSettings();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'help':
|
||||||
|
const helpToggle = document.getElementById('helpToggleBtn');
|
||||||
|
if (helpToggle) {
|
||||||
|
helpToggle.click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'notifications':
|
||||||
|
updateService.toggleUpdateModal();
|
||||||
|
break;
|
||||||
|
case 'support':
|
||||||
|
if (window.modalManager) {
|
||||||
|
window.modalManager.toggleModal('supportModal');
|
||||||
|
renderSupporters().catch(error => {
|
||||||
|
console.error('Error loading supporters:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHamburgerThemeIcon() {
|
||||||
|
const themeItem = document.querySelector('.dropdown-item[data-action="theme"]');
|
||||||
|
if (!themeItem) return;
|
||||||
|
|
||||||
|
const currentTheme = getStorageItem('theme') || 'auto';
|
||||||
|
const icon = themeItem.querySelector('i');
|
||||||
|
const text = themeItem.querySelector('span');
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
icon.classList.remove('fa-moon', 'fa-sun', 'fa-adjust');
|
||||||
|
if (currentTheme === 'light') {
|
||||||
|
icon.classList.add('fa-sun');
|
||||||
|
} else if (currentTheme === 'dark') {
|
||||||
|
icon.classList.add('fa-moon');
|
||||||
|
} else {
|
||||||
|
icon.classList.add('fa-adjust');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update text based on current theme
|
||||||
|
if (text) {
|
||||||
|
const key = currentTheme === 'light' ? 'header.theme.switchToDark' :
|
||||||
|
currentTheme === 'dark' ? 'header.theme.switchToLight' :
|
||||||
|
'header.theme.toggle';
|
||||||
|
updateElementAttribute(themeItem, 'aria-label', key, {}, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderForPage() {
|
updateHeaderForPage() {
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ function isEarlyAccessActive(version) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDownloadAllowed(version) {
|
||||||
|
if (!version.usageControl) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return version.usageControl === 'Download';
|
||||||
|
}
|
||||||
|
|
||||||
function buildMetaMarkup(version, options = {}) {
|
function buildMetaMarkup(version, options = {}) {
|
||||||
const segments = [];
|
const segments = [];
|
||||||
if (version.baseModel) {
|
if (version.baseModel) {
|
||||||
@@ -230,12 +237,17 @@ function buildBadge(label, tone, options = {}) {
|
|||||||
function buildActionButton(label, variant, action, options = {}) {
|
function buildActionButton(label, variant, action, options = {}) {
|
||||||
const attributes = [
|
const attributes = [
|
||||||
`class="version-action ${variant}"`,
|
`class="version-action ${variant}"`,
|
||||||
`data-version-action="${escapeHtml(action)}"`,
|
|
||||||
];
|
];
|
||||||
|
if (action) {
|
||||||
|
attributes.push(`data-version-action="${escapeHtml(action)}"`);
|
||||||
|
}
|
||||||
if (options.title) {
|
if (options.title) {
|
||||||
attributes.push(`title="${escapeHtml(options.title)}"`);
|
attributes.push(`title="${escapeHtml(options.title)}"`);
|
||||||
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
|
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
|
||||||
}
|
}
|
||||||
|
if (options.disabled) {
|
||||||
|
attributes.push('disabled');
|
||||||
|
}
|
||||||
if (options.extraAttributes) {
|
if (options.extraAttributes) {
|
||||||
attributes.push(options.extraAttributes);
|
attributes.push(options.extraAttributes);
|
||||||
}
|
}
|
||||||
@@ -371,6 +383,9 @@ function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
|||||||
if (hideEarlyAccess && isEarlyAccessActive(version)) {
|
if (hideEarlyAccess && isEarlyAccessActive(version)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!isDownloadAllowed(version)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const versionBase = normalizeBaseModelName(version.baseModel);
|
const versionBase = normalizeBaseModelName(version.baseModel);
|
||||||
if (versionBase !== normalizedBase) {
|
if (versionBase !== normalizedBase) {
|
||||||
return false;
|
return false;
|
||||||
@@ -502,6 +517,17 @@ function renderRow(version, options) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isDownloadAllowed(version)) {
|
||||||
|
const onSiteOnlyBadgeLabel = translate('modals.model.versions.badges.onSiteOnly', {}, 'On-Site Only');
|
||||||
|
badges.push(buildBadge(onSiteOnlyBadgeLabel, 'info', {
|
||||||
|
title: translate(
|
||||||
|
'modals.model.versions.badges.onSiteOnlyTooltip',
|
||||||
|
{},
|
||||||
|
'This version is only available for on-site generation on Civitai'
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (version.shouldIgnore) {
|
if (version.shouldIgnore) {
|
||||||
badges.push(buildBadge(ignoredBadgeLabel, 'muted', {
|
badges.push(buildBadge(ignoredBadgeLabel, 'muted', {
|
||||||
title: translate(
|
title: translate(
|
||||||
@@ -524,25 +550,36 @@ function renderRow(version, options) {
|
|||||||
|
|
||||||
const actions = [];
|
const actions = [];
|
||||||
if (!version.isInLibrary) {
|
if (!version.isInLibrary) {
|
||||||
// Download button with optional EA bolt icon
|
const canDownload = isDownloadAllowed(version);
|
||||||
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
||||||
actions.push(buildActionButton(
|
let downloadTitle;
|
||||||
downloadLabel,
|
if (!canDownload) {
|
||||||
'version-action-primary',
|
downloadTitle = translate(
|
||||||
'download',
|
'modals.model.versions.actions.downloadNotAllowedTooltip',
|
||||||
{
|
{},
|
||||||
title: isEarlyAccess
|
'This version is only available for on-site generation on Civitai'
|
||||||
? translate(
|
);
|
||||||
|
} else if (isEarlyAccess) {
|
||||||
|
downloadTitle = translate(
|
||||||
'modals.model.versions.actions.downloadEarlyAccessTooltip',
|
'modals.model.versions.actions.downloadEarlyAccessTooltip',
|
||||||
{},
|
{},
|
||||||
'Download this early access version from Civitai'
|
'Download this early access version from Civitai'
|
||||||
)
|
);
|
||||||
: translate(
|
} else {
|
||||||
|
downloadTitle = translate(
|
||||||
'modals.model.versions.actions.downloadTooltip',
|
'modals.model.versions.actions.downloadTooltip',
|
||||||
{},
|
{},
|
||||||
'Download this version'
|
'Download this version'
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
actions.push(buildActionButton(
|
||||||
|
downloadLabel,
|
||||||
|
canDownload ? 'version-action-primary' : 'version-action-disabled',
|
||||||
|
canDownload ? 'download' : '',
|
||||||
|
{
|
||||||
|
title: downloadTitle,
|
||||||
iconMarkup: downloadIcon,
|
iconMarkup: downloadIcon,
|
||||||
|
disabled: !canDownload,
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
} else if (version.filePath) {
|
} else if (version.filePath) {
|
||||||
|
|||||||
@@ -104,69 +104,74 @@ export class VirtualScroller {
|
|||||||
// Get display density setting
|
// Get display density setting
|
||||||
const displayDensity = state.global.settings?.display_density || 'default';
|
const displayDensity = state.global.settings?.display_density || 'default';
|
||||||
|
|
||||||
// Set exact column counts and grid widths to match CSS container widths
|
// Base gap between cards
|
||||||
let maxColumns, maxGridWidth;
|
const baseGap = 12;
|
||||||
|
this.columnGap = baseGap;
|
||||||
|
|
||||||
// Match exact column counts and CSS container width values based on density
|
// Define minimum card width based on density setting to ensure usability
|
||||||
|
// Cards smaller than this become hard to interact with and view
|
||||||
|
const minCardWidths = {
|
||||||
|
'default': 240, // Default: comfortable minimum
|
||||||
|
'medium': 200, // Medium: slightly smaller
|
||||||
|
'compact': 170 // Compact: smallest usable size
|
||||||
|
};
|
||||||
|
const minCardWidth = minCardWidths[displayDensity] || 240;
|
||||||
|
|
||||||
|
// Calculate maximum possible columns that fit in available width
|
||||||
|
// Formula: maxColumns = floor((availableWidth + gap) / (minCardWidth + gap))
|
||||||
|
const maxPossibleColumns = Math.floor((availableContentWidth + this.columnGap) / (minCardWidth + this.columnGap));
|
||||||
|
|
||||||
|
// Ensure at least 1 column
|
||||||
|
const maxColumns = Math.max(1, maxPossibleColumns);
|
||||||
|
|
||||||
|
// Define preferred maximum columns based on display density and screen size
|
||||||
|
// These are upper limits to prevent too many columns on ultra-wide screens
|
||||||
|
let preferredMaxColumns;
|
||||||
if (window.innerWidth >= 3000) { // 4K
|
if (window.innerWidth >= 3000) { // 4K
|
||||||
if (displayDensity === 'default') {
|
if (displayDensity === 'default') {
|
||||||
maxColumns = 8;
|
preferredMaxColumns = 8;
|
||||||
} else if (displayDensity === 'medium') {
|
} else if (displayDensity === 'medium') {
|
||||||
maxColumns = 9;
|
preferredMaxColumns = 10;
|
||||||
} else { // compact
|
} else { // compact
|
||||||
maxColumns = 10;
|
preferredMaxColumns = 12;
|
||||||
}
|
}
|
||||||
maxGridWidth = 2400; // Match exact CSS container width for 4K
|
|
||||||
} else if (window.innerWidth >= 2150) { // 2K/1440p
|
} else if (window.innerWidth >= 2150) { // 2K/1440p
|
||||||
if (displayDensity === 'default') {
|
if (displayDensity === 'default') {
|
||||||
maxColumns = 6;
|
preferredMaxColumns = 6;
|
||||||
} else if (displayDensity === 'medium') {
|
} else if (displayDensity === 'medium') {
|
||||||
maxColumns = 7;
|
preferredMaxColumns = 8;
|
||||||
} else { // compact
|
} else { // compact
|
||||||
maxColumns = 8;
|
preferredMaxColumns = 10;
|
||||||
}
|
}
|
||||||
maxGridWidth = 1800; // Match exact CSS container width for 2K
|
} else { // 1080p and smaller
|
||||||
} else {
|
|
||||||
// 1080p
|
|
||||||
if (displayDensity === 'default') {
|
if (displayDensity === 'default') {
|
||||||
maxColumns = 5;
|
preferredMaxColumns = 5;
|
||||||
} else if (displayDensity === 'medium') {
|
} else if (displayDensity === 'medium') {
|
||||||
maxColumns = 6;
|
preferredMaxColumns = 6;
|
||||||
} else { // compact
|
} else { // compact
|
||||||
maxColumns = 7;
|
preferredMaxColumns = 8;
|
||||||
}
|
}
|
||||||
maxGridWidth = 1400; // Match exact CSS container width for 1080p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate baseCardWidth based on desired column count and available space
|
// Use the smaller of: max columns that fit, or preferred max
|
||||||
// Formula: (maxGridWidth - (columns-1)*gap) / columns
|
// This ensures cards are never smaller than minCardWidth
|
||||||
const baseCardWidth = (maxGridWidth - ((maxColumns - 1) * this.columnGap)) / maxColumns;
|
this.columnsCount = Math.min(maxColumns, preferredMaxColumns);
|
||||||
|
|
||||||
// Use the smaller of available content width or max grid width
|
// Calculate card width to perfectly fill available space
|
||||||
const actualGridWidth = Math.min(availableContentWidth, maxGridWidth);
|
// Formula: (availableWidth - totalGap) / columns
|
||||||
|
const totalGap = (this.columnsCount - 1) * this.columnGap;
|
||||||
|
this.itemWidth = (availableContentWidth - totalGap) / this.columnsCount;
|
||||||
|
|
||||||
// Set exact column count based on screen size and mode
|
// Calculate height based on aspect ratio (896/1152)
|
||||||
this.columnsCount = maxColumns;
|
|
||||||
|
|
||||||
// When available width is smaller than maxGridWidth, recalculate columns
|
|
||||||
if (availableContentWidth < maxGridWidth) {
|
|
||||||
// Calculate how many columns can fit in the available space
|
|
||||||
this.columnsCount = Math.max(1, Math.floor(
|
|
||||||
(availableContentWidth + this.columnGap) / (baseCardWidth + this.columnGap)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate actual item width
|
|
||||||
this.itemWidth = (actualGridWidth - (this.columnsCount - 1) * this.columnGap) / this.columnsCount;
|
|
||||||
|
|
||||||
// Calculate height based on aspect ratio
|
|
||||||
this.itemHeight = this.itemWidth / this.itemAspectRatio;
|
this.itemHeight = this.itemWidth / this.itemAspectRatio;
|
||||||
|
|
||||||
// Calculate the left offset to center the grid within the content area
|
// Edge-to-edge layout: no offset, grid fills container
|
||||||
this.leftOffset = Math.max(0, (availableContentWidth - actualGridWidth) / 2);
|
this.leftOffset = 0;
|
||||||
|
const actualGridWidth = this.itemWidth * this.columnsCount + totalGap;
|
||||||
|
|
||||||
// Update grid element max-width to match available width
|
// Update grid element to fill available width
|
||||||
this.gridElement.style.maxWidth = `${actualGridWidth}px`;
|
this.gridElement.style.maxWidth = `${actualGridWidth}px`;
|
||||||
|
this.gridElement.style.width = `${actualGridWidth}px`;
|
||||||
|
|
||||||
// Add or remove density classes for style adjustments
|
// Add or remove density classes for style adjustments
|
||||||
this.gridElement.classList.remove('default-density', 'medium-density', 'compact-density');
|
this.gridElement.classList.remove('default-density', 'medium-density', 'compact-density');
|
||||||
@@ -478,6 +483,12 @@ export class VirtualScroller {
|
|||||||
element.style.width = `${this.itemWidth}px`;
|
element.style.width = `${this.itemWidth}px`;
|
||||||
element.style.height = `${this.itemHeight}px`;
|
element.style.height = `${this.itemHeight}px`;
|
||||||
|
|
||||||
|
// Remove max-width constraint from model-card to allow dynamic sizing
|
||||||
|
const modelCard = element.querySelector('.model-card');
|
||||||
|
if (modelCard) {
|
||||||
|
modelCard.style.maxWidth = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'components/controls.html' %}
|
{% include 'components/controls.html' %}
|
||||||
|
{% include 'components/breadcrumb.html' %}
|
||||||
{% include 'components/duplicates_banner.html' %}
|
{% include 'components/duplicates_banner.html' %}
|
||||||
{% include 'components/folder_sidebar.html' %}
|
{% include 'components/folder_sidebar.html' %}
|
||||||
|
|
||||||
|
|||||||
5
templates/components/breadcrumb.html
Normal file
5
templates/components/breadcrumb.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div id="breadcrumbContainer" class="sidebar-breadcrumb-container">
|
||||||
|
<nav class="sidebar-breadcrumb-nav" id="sidebarBreadcrumbNav">
|
||||||
|
<!-- Breadcrumbs will be populated by JavaScript -->
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
@@ -129,11 +129,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumb Navigation -->
|
|
||||||
<div id="breadcrumbContainer" class="sidebar-breadcrumb-container">
|
|
||||||
<nav class="sidebar-breadcrumb-nav" id="sidebarBreadcrumbNav">
|
|
||||||
<!-- Breadcrumbs will be populated by JavaScript -->
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
|
<!-- Left section: Logo + Navigation -->
|
||||||
|
<div class="header-left">
|
||||||
<div class="header-branding">
|
<div class="header-branding">
|
||||||
<a href="/loras" class="logo-link">
|
<a href="/loras" class="logo-link">
|
||||||
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
||||||
@@ -18,10 +20,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set current_page = 'loras' %}
|
{% set current_page = 'loras' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set search_disabled = current_page == 'statistics' %}
|
|
||||||
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
|
|
||||||
current_page %}
|
|
||||||
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
|
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem">
|
<a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem">
|
||||||
<i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span>
|
<i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span>
|
||||||
@@ -43,8 +41,13 @@
|
|||||||
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Context-aware search container -->
|
<!-- Center section: Search -->
|
||||||
|
{% set search_disabled = current_page == 'statistics' %}
|
||||||
|
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
|
||||||
|
current_page %}
|
||||||
|
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
|
||||||
<div class="{{ header_search_class }}" id="headerSearch">
|
<div class="{{ header_search_class }}" id="headerSearch">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
|
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
|
||||||
@@ -62,9 +65,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-actions">
|
<!-- Right section: Controls -->
|
||||||
<!-- Integrated corner controls -->
|
<div class="header-right">
|
||||||
<div class="header-controls">
|
<div class="header-controls" id="headerControls">
|
||||||
<div class="theme-toggle" title="{{ t('header.theme.toggle') }}">
|
<div class="theme-toggle" title="{{ t('header.theme.toggle') }}">
|
||||||
<i class="fas fa-moon dark-icon"></i>
|
<i class="fas fa-moon dark-icon"></i>
|
||||||
<i class="fas fa-sun light-icon"></i>
|
<i class="fas fa-sun light-icon"></i>
|
||||||
@@ -85,6 +88,34 @@
|
|||||||
<i class="fas fa-heart"></i>
|
<i class="fas fa-heart"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Hamburger menu button (visible on mobile) -->
|
||||||
|
<button class="hamburger-menu-btn" id="hamburgerMenuBtn" title="{{ t('common.actions.menu') }}">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<!-- Hamburger dropdown menu -->
|
||||||
|
<div class="hamburger-dropdown" id="hamburgerDropdown">
|
||||||
|
<div class="dropdown-item theme-toggle-item" data-action="theme">
|
||||||
|
<i class="fas fa-moon"></i>
|
||||||
|
<span>{{ t('header.theme.toggle') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item" data-action="settings">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
<span>{{ t('common.actions.settings') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item" data-action="help">
|
||||||
|
<i class="fas fa-question-circle"></i>
|
||||||
|
<span>{{ t('common.actions.help') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item" data-action="notifications">
|
||||||
|
<i class="fas fa-bell"></i>
|
||||||
|
<span>{{ t('header.actions.notifications') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<div class="dropdown-item" data-action="support">
|
||||||
|
<i class="fas fa-heart"></i>
|
||||||
|
<span>{{ t('header.actions.support') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'components/controls.html' %}
|
{% include 'components/controls.html' %}
|
||||||
|
{% include 'components/breadcrumb.html' %}
|
||||||
{% include 'components/duplicates_banner.html' %}
|
{% include 'components/duplicates_banner.html' %}
|
||||||
{% include 'components/folder_sidebar.html' %}
|
{% include 'components/folder_sidebar.html' %}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'components/controls.html' %}
|
{% include 'components/controls.html' %}
|
||||||
|
{% include 'components/breadcrumb.html' %}
|
||||||
{% include 'components/duplicates_banner.html' %}
|
{% include 'components/duplicates_banner.html' %}
|
||||||
{% include 'components/folder_sidebar.html' %}
|
{% include 'components/folder_sidebar.html' %}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user