mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -03:00
feat: enhance model version context with file metadata
- Rename `preview_overrides` to `version_context` to better reflect expanded purpose - Add file_path and file_name fields to version serialization - Update method names and parameter signatures for consistency - Include file metadata from cache in version context building - Maintain backward compatibility with existing preview URL functionality The changes provide more comprehensive version information including file details while maintaining existing preview override behavior.
This commit is contained in:
@@ -854,13 +854,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "Beispiele",
|
"examples": "Beispiele",
|
||||||
"description": "Modellbeschreibung",
|
"description": "Modellbeschreibung",
|
||||||
"recipes": "Rezepte"
|
"recipes": "Rezepte",
|
||||||
|
"versions": "Versionen"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "Beispielbilder werden geladen...",
|
"exampleImages": "Beispielbilder werden geladen...",
|
||||||
"description": "Modellbeschreibung wird geladen...",
|
"description": "Modellbeschreibung wird geladen...",
|
||||||
"recipes": "Rezepte werden geladen...",
|
"recipes": "Rezepte werden geladen...",
|
||||||
"examples": "Beispiele werden geladen..."
|
"examples": "Beispiele werden geladen...",
|
||||||
|
"versions": "Versionen werden geladen..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "Modellversionen",
|
||||||
|
"copy": "Verwalten Sie alle Versionen dieses Modells an einem Ort.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "Keine Vorschau"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "Unbenannte Version",
|
||||||
|
"noDetails": "Keine zusätzlichen Details"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "Aktuelle Version",
|
||||||
|
"inLibrary": "In der Bibliothek",
|
||||||
|
"newer": "Neuere Version",
|
||||||
|
"ignored": "Ignoriert"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"ignore": "Ignorieren",
|
||||||
|
"unignore": "Ignorierung aufheben",
|
||||||
|
"resumeModelUpdates": "Aktualisierungen für dieses Modell fortsetzen",
|
||||||
|
"ignoreModelUpdates": "Aktualisierungen für dieses Modell ignorieren",
|
||||||
|
"viewLocalVersions": "Alle lokalen Versionen anzeigen",
|
||||||
|
"viewLocalTooltip": "Demnächst verfügbar"
|
||||||
|
},
|
||||||
|
"empty": "Noch keine Versionshistorie für dieses Modell vorhanden.",
|
||||||
|
"error": "Versionen konnten nicht geladen werden.",
|
||||||
|
"missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Diese Version aus Ihrer Bibliothek löschen?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "Aktualisierungen für dieses Modell werden ignoriert",
|
||||||
|
"modelResumed": "Aktualisierungen für dieses Modell werden wieder geprüft",
|
||||||
|
"versionIgnored": "Aktualisierungen für diese Version werden ignoriert",
|
||||||
|
"versionUnignored": "Version wurde wieder aktiviert",
|
||||||
|
"versionDeleted": "Version gelöscht"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "Examples",
|
"examples": "Examples",
|
||||||
"description": "Model Description",
|
"description": "Model Description",
|
||||||
"recipes": "Recipes"
|
"recipes": "Recipes",
|
||||||
|
"versions": "Versions"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "Loading example images...",
|
"exampleImages": "Loading example images...",
|
||||||
"description": "Loading model description...",
|
"description": "Loading model description...",
|
||||||
"recipes": "Loading recipes...",
|
"recipes": "Loading recipes...",
|
||||||
"examples": "Loading examples..."
|
"examples": "Loading examples...",
|
||||||
|
"versions": "Loading versions..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "Model versions",
|
||||||
|
"copy": "Track and manage every version of this model in one place.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "No preview"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "Untitled Version",
|
||||||
|
"noDetails": "No additional details"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "Current Version",
|
||||||
|
"inLibrary": "In Library",
|
||||||
|
"newer": "Newer Version",
|
||||||
|
"ignored": "Ignored"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "Download",
|
||||||
|
"delete": "Delete",
|
||||||
|
"ignore": "Ignore",
|
||||||
|
"unignore": "Unignore",
|
||||||
|
"resumeModelUpdates": "Resume updates for this model",
|
||||||
|
"ignoreModelUpdates": "Ignore updates for this model",
|
||||||
|
"viewLocalVersions": "View all local versions",
|
||||||
|
"viewLocalTooltip": "Coming soon"
|
||||||
|
},
|
||||||
|
"empty": "No version history available for this model yet.",
|
||||||
|
"error": "Failed to load versions.",
|
||||||
|
"missingModelId": "This model is missing a Civitai model id.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Delete this version from your library?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "Updates ignored for this model",
|
||||||
|
"modelResumed": "Update tracking resumed",
|
||||||
|
"versionIgnored": "Updates ignored for this version",
|
||||||
|
"versionUnignored": "Version re-enabled",
|
||||||
|
"versionDeleted": "Version deleted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "Ejemplos",
|
"examples": "Ejemplos",
|
||||||
"description": "Descripción del modelo",
|
"description": "Descripción del modelo",
|
||||||
"recipes": "Recetas"
|
"recipes": "Recetas",
|
||||||
|
"versions": "Versiones"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "Cargando imágenes de ejemplo...",
|
"exampleImages": "Cargando imágenes de ejemplo...",
|
||||||
"description": "Cargando descripción del modelo...",
|
"description": "Cargando descripción del modelo...",
|
||||||
"recipes": "Cargando recetas...",
|
"recipes": "Cargando recetas...",
|
||||||
"examples": "Cargando ejemplos..."
|
"examples": "Cargando ejemplos...",
|
||||||
|
"versions": "Cargando versiones..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "Versiones del modelo",
|
||||||
|
"copy": "Administra todas las versiones de este modelo en un solo lugar.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "Sin vista previa"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "Versión sin nombre",
|
||||||
|
"noDetails": "Sin detalles adicionales"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "Versión actual",
|
||||||
|
"inLibrary": "En la biblioteca",
|
||||||
|
"newer": "Versión más reciente",
|
||||||
|
"ignored": "Ignorada"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "Descargar",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"ignore": "Ignorar",
|
||||||
|
"unignore": "Dejar de ignorar",
|
||||||
|
"resumeModelUpdates": "Reanudar actualizaciones para este modelo",
|
||||||
|
"ignoreModelUpdates": "Ignorar actualizaciones para este modelo",
|
||||||
|
"viewLocalVersions": "Ver todas las versiones locales",
|
||||||
|
"viewLocalTooltip": "Disponible pronto"
|
||||||
|
},
|
||||||
|
"empty": "Aún no hay historial de versiones para este modelo.",
|
||||||
|
"error": "No se pudieron cargar las versiones.",
|
||||||
|
"missingModelId": "Este modelo no tiene un ID de modelo de Civitai.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "¿Eliminar esta versión de tu biblioteca?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "Se ignoran las actualizaciones de este modelo",
|
||||||
|
"modelResumed": "Seguimiento de actualizaciones reanudado",
|
||||||
|
"versionIgnored": "Se ignoran las actualizaciones de esta versión",
|
||||||
|
"versionUnignored": "Versión habilitada nuevamente",
|
||||||
|
"versionDeleted": "Versión eliminada"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "Exemples",
|
"examples": "Exemples",
|
||||||
"description": "Description du modèle",
|
"description": "Description du modèle",
|
||||||
"recipes": "Recipes"
|
"recipes": "Recipes",
|
||||||
|
"versions": "Versions"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "Chargement des images d'exemple...",
|
"exampleImages": "Chargement des images d'exemple...",
|
||||||
"description": "Chargement de la description du modèle...",
|
"description": "Chargement de la description du modèle...",
|
||||||
"recipes": "Chargement des recipes...",
|
"recipes": "Chargement des recipes...",
|
||||||
"examples": "Chargement des exemples..."
|
"examples": "Chargement des exemples...",
|
||||||
|
"versions": "Chargement des versions..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "Versions du modèle",
|
||||||
|
"copy": "Gérez toutes les versions de ce modèle en un seul endroit.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "Aucune prévisualisation"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "Version sans nom",
|
||||||
|
"noDetails": "Aucun détail supplémentaire"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "Version actuelle",
|
||||||
|
"inLibrary": "Dans la bibliothèque",
|
||||||
|
"newer": "Version plus récente",
|
||||||
|
"ignored": "Ignorée"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "Télécharger",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"ignore": "Ignorer",
|
||||||
|
"unignore": "Ne plus ignorer",
|
||||||
|
"resumeModelUpdates": "Reprendre les mises à jour pour ce modèle",
|
||||||
|
"ignoreModelUpdates": "Ignorer les mises à jour pour ce modèle",
|
||||||
|
"viewLocalVersions": "Voir toutes les versions locales",
|
||||||
|
"viewLocalTooltip": "Bientôt disponible"
|
||||||
|
},
|
||||||
|
"empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.",
|
||||||
|
"error": "Échec du chargement des versions.",
|
||||||
|
"missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Supprimer cette version de votre bibliothèque ?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "Les mises à jour de ce modèle sont ignorées",
|
||||||
|
"modelResumed": "Suivi des mises à jour repris",
|
||||||
|
"versionIgnored": "Les mises à jour de cette version sont ignorées",
|
||||||
|
"versionUnignored": "Version réactivée",
|
||||||
|
"versionDeleted": "Version supprimée"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "דוגמאות",
|
"examples": "דוגמאות",
|
||||||
"description": "תיאור המודל",
|
"description": "תיאור המודל",
|
||||||
"recipes": "מתכונים"
|
"recipes": "מתכונים",
|
||||||
|
"versions": "גרסאות"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "טוען תמונות דוגמה...",
|
"exampleImages": "טוען תמונות דוגמה...",
|
||||||
"description": "טוען תיאור מודל...",
|
"description": "טוען תיאור מודל...",
|
||||||
"recipes": "טוען מתכונים...",
|
"recipes": "טוען מתכונים...",
|
||||||
"examples": "טוען דוגמאות..."
|
"examples": "טוען דוגמאות...",
|
||||||
|
"versions": "טוען גרסאות..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "גרסאות המודל",
|
||||||
|
"copy": "נהל את כל הגרסאות של המודל הזה במקום אחד.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "אין תצוגה מקדימה"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "גרסה ללא שם",
|
||||||
|
"noDetails": "אין פרטים נוספים"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "גרסה נוכחית",
|
||||||
|
"inLibrary": "בספרייה",
|
||||||
|
"newer": "גרסה חדשה יותר",
|
||||||
|
"ignored": "התעלם"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "הורדה",
|
||||||
|
"delete": "מחיקה",
|
||||||
|
"ignore": "התעלם",
|
||||||
|
"unignore": "בטל התעלמות",
|
||||||
|
"resumeModelUpdates": "המשך עדכונים עבור מודל זה",
|
||||||
|
"ignoreModelUpdates": "התעלם מעדכונים עבור מודל זה",
|
||||||
|
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",
|
||||||
|
"viewLocalTooltip": "יגיע בקרוב"
|
||||||
|
},
|
||||||
|
"empty": "אין עדיין היסטוריית גרסאות למודל זה.",
|
||||||
|
"error": "טעינת הגרסאות נכשלה.",
|
||||||
|
"missingModelId": "למודל זה אין מזהה מודל של Civitai.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "למחוק גרסה זו מהספרייה שלך?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "העדכונים עבור מודל זה נוגבו",
|
||||||
|
"modelResumed": "מעקב העדכונים חודש",
|
||||||
|
"versionIgnored": "העדכונים עבור גרסה זו נוגבו",
|
||||||
|
"versionUnignored": "הגרסה הופעלה מחדש",
|
||||||
|
"versionDeleted": "הגרסה נמחקה"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "例",
|
"examples": "例",
|
||||||
"description": "モデル説明",
|
"description": "モデル説明",
|
||||||
"recipes": "レシピ"
|
"recipes": "レシピ",
|
||||||
|
"versions": "バージョン"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "例画像を読み込み中...",
|
"exampleImages": "例画像を読み込み中...",
|
||||||
"description": "モデル説明を読み込み中...",
|
"description": "モデル説明を読み込み中...",
|
||||||
"recipes": "レシピを読み込み中...",
|
"recipes": "レシピを読み込み中...",
|
||||||
"examples": "例を読み込み中..."
|
"examples": "例を読み込み中...",
|
||||||
|
"versions": "バージョンを読み込み中..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "モデルバージョン",
|
||||||
|
"copy": "このモデルのすべてのバージョンを一か所で管理します。",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "プレビューなし"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "名前のないバージョン",
|
||||||
|
"noDetails": "追加情報なし"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "現在のバージョン",
|
||||||
|
"inLibrary": "ライブラリにあります",
|
||||||
|
"newer": "新しいバージョン",
|
||||||
|
"ignored": "無視中"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "ダウンロード",
|
||||||
|
"delete": "削除",
|
||||||
|
"ignore": "無視",
|
||||||
|
"unignore": "無視を解除",
|
||||||
|
"resumeModelUpdates": "このモデルの更新を再開",
|
||||||
|
"ignoreModelUpdates": "このモデルの更新を無視",
|
||||||
|
"viewLocalVersions": "ローカルの全バージョンを表示",
|
||||||
|
"viewLocalTooltip": "近日対応予定"
|
||||||
|
},
|
||||||
|
"empty": "このモデルにはまだバージョン履歴がありません。",
|
||||||
|
"error": "バージョンの読み込みに失敗しました。",
|
||||||
|
"missingModelId": "このモデルにはCivitaiのモデルIDがありません。",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "このバージョンをライブラリから削除しますか?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "このモデルの更新は無視されます",
|
||||||
|
"modelResumed": "更新の監視を再開しました",
|
||||||
|
"versionIgnored": "このバージョンの更新は無視されます",
|
||||||
|
"versionUnignored": "バージョンを再度有効にしました",
|
||||||
|
"versionDeleted": "バージョンを削除しました"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "예시",
|
"examples": "예시",
|
||||||
"description": "모델 설명",
|
"description": "모델 설명",
|
||||||
"recipes": "레시피"
|
"recipes": "레시피",
|
||||||
|
"versions": "버전"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "예시 이미지 로딩 중...",
|
"exampleImages": "예시 이미지 로딩 중...",
|
||||||
"description": "모델 설명 로딩 중...",
|
"description": "모델 설명 로딩 중...",
|
||||||
"recipes": "레시피 로딩 중...",
|
"recipes": "레시피 로딩 중...",
|
||||||
"examples": "예시 로딩 중..."
|
"examples": "예시 로딩 중...",
|
||||||
|
"versions": "버전 로딩 중..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "모델 버전",
|
||||||
|
"copy": "이 모델의 모든 버전을 한 곳에서 관리하세요.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "미리보기 없음"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "이름 없는 버전",
|
||||||
|
"noDetails": "추가 정보 없음"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "현재 버전",
|
||||||
|
"inLibrary": "라이브러리에 있음",
|
||||||
|
"newer": "최신 버전",
|
||||||
|
"ignored": "무시됨"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "다운로드",
|
||||||
|
"delete": "삭제",
|
||||||
|
"ignore": "무시",
|
||||||
|
"unignore": "무시 해제",
|
||||||
|
"resumeModelUpdates": "이 모델 업데이트 재개",
|
||||||
|
"ignoreModelUpdates": "이 모델 업데이트 무시",
|
||||||
|
"viewLocalVersions": "로컬 버전 모두 보기",
|
||||||
|
"viewLocalTooltip": "곧 제공 예정"
|
||||||
|
},
|
||||||
|
"empty": "이 모델에는 아직 버전 기록이 없습니다.",
|
||||||
|
"error": "버전을 불러오지 못했습니다.",
|
||||||
|
"missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "이 버전을 라이브러리에서 삭제하시겠습니까?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "이 모델의 업데이트가 무시됩니다",
|
||||||
|
"modelResumed": "업데이트 추적이 재개되었습니다",
|
||||||
|
"versionIgnored": "이 버전의 업데이트가 무시됩니다",
|
||||||
|
"versionUnignored": "버전이 다시 활성화되었습니다",
|
||||||
|
"versionDeleted": "버전이 삭제되었습니다"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "Примеры",
|
"examples": "Примеры",
|
||||||
"description": "Описание модели",
|
"description": "Описание модели",
|
||||||
"recipes": "Рецепты"
|
"recipes": "Рецепты",
|
||||||
|
"versions": "Версии"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "Загрузка примеров изображений...",
|
"exampleImages": "Загрузка примеров изображений...",
|
||||||
"description": "Загрузка описания модели...",
|
"description": "Загрузка описания модели...",
|
||||||
"recipes": "Загрузка рецептов...",
|
"recipes": "Загрузка рецептов...",
|
||||||
"examples": "Загрузка примеров..."
|
"examples": "Загрузка примеров...",
|
||||||
|
"versions": "Загрузка версий..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "Версии модели",
|
||||||
|
"copy": "Управляйте всеми версиями этой модели в одном месте.",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "Нет превью"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "Версия без названия",
|
||||||
|
"noDetails": "Дополнительная информация отсутствует"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "Текущая версия",
|
||||||
|
"inLibrary": "В библиотеке",
|
||||||
|
"newer": "Более новая версия",
|
||||||
|
"ignored": "Игнорируется"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "Скачать",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"ignore": "Игнорировать",
|
||||||
|
"unignore": "Перестать игнорировать",
|
||||||
|
"resumeModelUpdates": "Возобновить обновления для этой модели",
|
||||||
|
"ignoreModelUpdates": "Игнорировать обновления для этой модели",
|
||||||
|
"viewLocalVersions": "Показать все локальные версии",
|
||||||
|
"viewLocalTooltip": "Скоро появится"
|
||||||
|
},
|
||||||
|
"empty": "Для этой модели пока нет истории версий.",
|
||||||
|
"error": "Не удалось загрузить версии.",
|
||||||
|
"missingModelId": "У этой модели отсутствует идентификатор модели Civitai.",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Удалить эту версию из библиотеки?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "Обновления для этой модели игнорируются",
|
||||||
|
"modelResumed": "Отслеживание обновлений возобновлено",
|
||||||
|
"versionIgnored": "Обновления для этой версии игнорируются",
|
||||||
|
"versionUnignored": "Версия снова активна",
|
||||||
|
"versionDeleted": "Версия удалена"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "示例",
|
"examples": "示例",
|
||||||
"description": "模型描述",
|
"description": "模型描述",
|
||||||
"recipes": "配方"
|
"recipes": "配方",
|
||||||
|
"versions": "版本"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "正在加载示例图片...",
|
"exampleImages": "正在加载示例图片...",
|
||||||
"description": "正在加载模型描述...",
|
"description": "正在加载模型描述...",
|
||||||
"recipes": "正在加载配方...",
|
"recipes": "正在加载配方...",
|
||||||
"examples": "正在加载示例..."
|
"examples": "正在加载示例...",
|
||||||
|
"versions": "正在加载版本..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "模型版本",
|
||||||
|
"copy": "在一个位置管理该模型的所有版本。",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "无预览"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "未命名版本",
|
||||||
|
"noDetails": "暂无更多信息"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "当前版本",
|
||||||
|
"inLibrary": "已在库中",
|
||||||
|
"newer": "较新的版本",
|
||||||
|
"ignored": "已忽略"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "下载",
|
||||||
|
"delete": "删除",
|
||||||
|
"ignore": "忽略",
|
||||||
|
"unignore": "取消忽略",
|
||||||
|
"resumeModelUpdates": "继续跟踪该模型的更新",
|
||||||
|
"ignoreModelUpdates": "忽略该模型的更新",
|
||||||
|
"viewLocalVersions": "查看所有本地版本",
|
||||||
|
"viewLocalTooltip": "敬请期待"
|
||||||
|
},
|
||||||
|
"empty": "该模型还没有版本历史。",
|
||||||
|
"error": "加载版本失败。",
|
||||||
|
"missingModelId": "该模型缺少 Civitai 模型 ID。",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "从库中删除此版本?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "已忽略该模型的更新",
|
||||||
|
"modelResumed": "已恢复更新跟踪",
|
||||||
|
"versionIgnored": "已忽略该版本的更新",
|
||||||
|
"versionUnignored": "已重新启用该版本",
|
||||||
|
"versionDeleted": "版本已删除"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -853,13 +853,55 @@
|
|||||||
"tabs": {
|
"tabs": {
|
||||||
"examples": "範例圖片",
|
"examples": "範例圖片",
|
||||||
"description": "模型描述",
|
"description": "模型描述",
|
||||||
"recipes": "配方"
|
"recipes": "配方",
|
||||||
|
"versions": "版本"
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
"exampleImages": "載入範例圖片中...",
|
"exampleImages": "載入範例圖片中...",
|
||||||
"description": "載入模型描述中...",
|
"description": "載入模型描述中...",
|
||||||
"recipes": "載入配方中...",
|
"recipes": "載入配方中...",
|
||||||
"examples": "載入範例中..."
|
"examples": "載入範例中...",
|
||||||
|
"versions": "載入版本中..."
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"heading": "模型版本",
|
||||||
|
"copy": "在同一位置追蹤並管理此模型的所有版本。",
|
||||||
|
"media": {
|
||||||
|
"placeholder": "無預覽"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"unnamed": "未命名版本",
|
||||||
|
"noDetails": "沒有其他資訊"
|
||||||
|
},
|
||||||
|
"badges": {
|
||||||
|
"current": "目前版本",
|
||||||
|
"inLibrary": "已在庫中",
|
||||||
|
"newer": "較新版本",
|
||||||
|
"ignored": "已忽略"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"download": "下載",
|
||||||
|
"delete": "刪除",
|
||||||
|
"ignore": "忽略",
|
||||||
|
"unignore": "取消忽略",
|
||||||
|
"resumeModelUpdates": "恢復追蹤此模型的更新",
|
||||||
|
"ignoreModelUpdates": "忽略此模型的更新",
|
||||||
|
"viewLocalVersions": "檢視所有本地版本",
|
||||||
|
"viewLocalTooltip": "敬請期待"
|
||||||
|
},
|
||||||
|
"empty": "此模型尚無版本歷史。",
|
||||||
|
"error": "載入版本失敗。",
|
||||||
|
"missingModelId": "此模型缺少 Civitai 模型 ID。",
|
||||||
|
"confirm": {
|
||||||
|
"delete": "要從庫中刪除此版本嗎?"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"modelIgnored": "已忽略此模型的更新",
|
||||||
|
"modelResumed": "已恢復更新追蹤",
|
||||||
|
"versionIgnored": "已忽略此版本的更新",
|
||||||
|
"versionUnignored": "已重新啟用此版本",
|
||||||
|
"versionDeleted": "已刪除此版本"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1108,9 +1108,9 @@ class ModelUpdateHandler:
|
|||||||
version_id,
|
version_id,
|
||||||
should_ignore,
|
should_ignore,
|
||||||
)
|
)
|
||||||
overrides = await self._build_preview_overrides(record)
|
overrides = await self._build_version_context(record)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": True, "record": self._serialize_record(record, preview_overrides=overrides)}
|
{"success": True, "record": self._serialize_record(record, version_context=overrides)}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_model_update_status(self, request: web.Request) -> web.Response:
|
async def get_model_update_status(self, request: web.Request) -> web.Response:
|
||||||
@@ -1157,9 +1157,9 @@ class ModelUpdateHandler:
|
|||||||
{"success": False, "error": "Model not tracked"}, status=404
|
{"success": False, "error": "Model not tracked"}, status=404
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides = await self._build_preview_overrides(record)
|
overrides = await self._build_version_context(record)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": True, "record": self._serialize_record(record, preview_overrides=overrides)}
|
{"success": True, "record": self._serialize_record(record, version_context=overrides)}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _get_or_refresh_record(
|
async def _get_or_refresh_record(
|
||||||
@@ -1219,9 +1219,9 @@ class ModelUpdateHandler:
|
|||||||
self,
|
self,
|
||||||
record,
|
record,
|
||||||
*,
|
*,
|
||||||
preview_overrides: Optional[Dict[int, Optional[str]]] = None,
|
version_context: Optional[Dict[int, Dict[str, Optional[str]]]] = None,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
overrides = preview_overrides or {}
|
context = version_context or {}
|
||||||
return {
|
return {
|
||||||
"modelType": record.model_type,
|
"modelType": record.model_type,
|
||||||
"modelId": record.model_id,
|
"modelId": record.model_id,
|
||||||
@@ -1232,14 +1232,16 @@ class ModelUpdateHandler:
|
|||||||
"shouldIgnore": record.should_ignore_model,
|
"shouldIgnore": record.should_ignore_model,
|
||||||
"hasUpdate": record.has_update(),
|
"hasUpdate": record.has_update(),
|
||||||
"versions": [
|
"versions": [
|
||||||
self._serialize_version(version, overrides.get(version.version_id))
|
self._serialize_version(version, context.get(version.version_id))
|
||||||
for version in record.versions
|
for version in record.versions
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _serialize_version(version, override_preview: Optional[str]) -> Dict:
|
def _serialize_version(version, context: Optional[Dict[str, Optional[str]]]) -> Dict:
|
||||||
preview_url = override_preview if override_preview is not None else version.preview_url
|
context = context or {}
|
||||||
|
preview_override = context.get("preview_override")
|
||||||
|
preview_url = preview_override if preview_override is not None else version.preview_url
|
||||||
return {
|
return {
|
||||||
"versionId": version.version_id,
|
"versionId": version.version_id,
|
||||||
"name": version.name,
|
"name": version.name,
|
||||||
@@ -1249,19 +1251,21 @@ class ModelUpdateHandler:
|
|||||||
"previewUrl": preview_url,
|
"previewUrl": preview_url,
|
||||||
"isInLibrary": version.is_in_library,
|
"isInLibrary": version.is_in_library,
|
||||||
"shouldIgnore": version.should_ignore,
|
"shouldIgnore": version.should_ignore,
|
||||||
|
"filePath": context.get("file_path"),
|
||||||
|
"fileName": context.get("file_name"),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _build_preview_overrides(self, record) -> Dict[int, Optional[str]]:
|
async def _build_version_context(self, record) -> Dict[int, Dict[str, Optional[str]]]:
|
||||||
overrides: Dict[int, Optional[str]] = {}
|
context: Dict[int, Dict[str, Optional[str]]] = {}
|
||||||
try:
|
try:
|
||||||
cache = await self._service.scanner.get_cached_data()
|
cache = await self._service.scanner.get_cached_data()
|
||||||
except Exception as exc: # pragma: no cover - defensive logging
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
self._logger.debug("Failed to load cache while building preview overrides: %s", exc)
|
self._logger.debug("Failed to load cache while building preview overrides: %s", exc)
|
||||||
return overrides
|
return context
|
||||||
|
|
||||||
version_index = getattr(cache, "version_index", None)
|
version_index = getattr(cache, "version_index", None)
|
||||||
if not version_index:
|
if not version_index:
|
||||||
return overrides
|
return context
|
||||||
|
|
||||||
for version in record.versions:
|
for version in record.versions:
|
||||||
if not version.is_in_library:
|
if not version.is_in_library:
|
||||||
@@ -1269,9 +1273,15 @@ class ModelUpdateHandler:
|
|||||||
cache_entry = version_index.get(version.version_id)
|
cache_entry = version_index.get(version.version_id)
|
||||||
if isinstance(cache_entry, Mapping):
|
if isinstance(cache_entry, Mapping):
|
||||||
preview = cache_entry.get("preview_url")
|
preview = cache_entry.get("preview_url")
|
||||||
|
context_entry: Dict[str, Optional[str]] = {
|
||||||
|
"file_path": cache_entry.get("file_path"),
|
||||||
|
"file_name": cache_entry.get("file_name"),
|
||||||
|
"preview_override": None,
|
||||||
|
}
|
||||||
if isinstance(preview, str) and preview:
|
if isinstance(preview, str) and preview:
|
||||||
overrides[version.version_id] = config.get_preview_static_url(preview)
|
context_entry["preview_override"] = config.get_preview_static_url(preview)
|
||||||
return overrides
|
context[version.version_id] = context_entry
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
335
static/css/components/lora-modal/versions.css
Normal file
335
static/css/components/lora-modal/versions.css
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
.model-versions-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: color-mix(in oklch, var(--lora-surface) 70%, transparent);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-info h3 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn {
|
||||||
|
appearance: none;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn-primary {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: color-mix(in oklch, var(--lora-accent) 70%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: color-mix(in oklch, var(--lora-accent) 85%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn-secondary:hover:not(:disabled) {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color);
|
||||||
|
margin: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-version-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 124px 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: color-mix(in oklch, var(--card-bg) 92%, var(--bg-color) 8%);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .model-version-row {
|
||||||
|
background: color-mix(in oklch, var(--card-bg) 88%, black 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-version-row:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-version-row.is-current {
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in oklch, var(--lora-accent) 65%, transparent),
|
||||||
|
0 10px 22px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-media {
|
||||||
|
width: 124px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--border-color) 70%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-media img,
|
||||||
|
.version-media video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-media video {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-media-placeholder {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: color-mix(in oklch, var(--text-color) 92%, black 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-id {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge-info {
|
||||||
|
background: color-mix(in oklch, var(--badge-update-bg) 25%, transparent);
|
||||||
|
color: var(--badge-update-bg);
|
||||||
|
border-color: color-mix(in oklch, var(--badge-update-bg) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge-success {
|
||||||
|
background: color-mix(in oklch, var(--lora-success) 25%, transparent);
|
||||||
|
color: var(--lora-success);
|
||||||
|
border-color: color-mix(in oklch, var(--lora-success) 50%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge-muted {
|
||||||
|
background: color-mix(in oklch, var(--text-muted) 18%, transparent);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-color: color-mix(in oklch, var(--text-muted) 40%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge-current {
|
||||||
|
background: color-mix(in oklch, var(--lora-accent) 22%, transparent);
|
||||||
|
color: var(--lora-accent);
|
||||||
|
border-color: color-mix(in oklch, var(--lora-accent) 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-meta-primary {
|
||||||
|
font-weight: 600;
|
||||||
|
color: color-mix(in oklch, var(--text-color) 88%, var(--lora-accent) 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-meta-separator {
|
||||||
|
color: color-mix(in oklch, var(--text-muted) 90%, var(--text-color) 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action {
|
||||||
|
min-width: 128px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-primary {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: #fff;
|
||||||
|
border-color: color-mix(in oklch, var(--lora-accent) 65%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: color-mix(in oklch, var(--lora-accent) 85%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-danger {
|
||||||
|
background: transparent;
|
||||||
|
border-color: color-mix(in oklch, var(--lora-error) 60%, transparent);
|
||||||
|
color: var(--lora-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-danger:hover {
|
||||||
|
background: color-mix(in oklch, var(--lora-error) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action-ghost:hover {
|
||||||
|
background: color-mix(in oklch, var(--lora-surface) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-loading-state,
|
||||||
|
.versions-empty,
|
||||||
|
.versions-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px dashed var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-error {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: color-mix(in oklch, var(--lora-error) 45%, transparent);
|
||||||
|
color: var(--lora-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-empty i,
|
||||||
|
.versions-error i {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.model-version-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-actions {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-action {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
@import 'components/lora-modal/preset-tags.css';
|
@import 'components/lora-modal/preset-tags.css';
|
||||||
@import 'components/lora-modal/showcase.css';
|
@import 'components/lora-modal/showcase.css';
|
||||||
@import 'components/lora-modal/triggerwords.css';
|
@import 'components/lora-modal/triggerwords.css';
|
||||||
|
@import 'components/lora-modal/versions.css';
|
||||||
@import 'components/shared/edit-metadata.css';
|
@import 'components/shared/edit-metadata.css';
|
||||||
@import 'components/search-filter.css';
|
@import 'components/search-filter.css';
|
||||||
@import 'components/bulk.css';
|
@import 'components/bulk.css';
|
||||||
@@ -55,4 +56,4 @@
|
|||||||
/* 使用已有的loading-spinner样式 */
|
/* 使用已有的loading-spinner样式 */
|
||||||
.initialization-notice .loading-spinner {
|
.initialization-notice .loading-spinner {
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export function getApiEndpoints(modelType) {
|
|||||||
relinkCivitai: `/api/lm/${modelType}/relink-civitai`,
|
relinkCivitai: `/api/lm/${modelType}/relink-civitai`,
|
||||||
civitaiVersions: `/api/lm/${modelType}/civitai/versions`,
|
civitaiVersions: `/api/lm/${modelType}/civitai/versions`,
|
||||||
refreshUpdates: `/api/lm/${modelType}/updates/refresh`,
|
refreshUpdates: `/api/lm/${modelType}/updates/refresh`,
|
||||||
|
modelUpdateStatus: `/api/lm/${modelType}/updates/status`,
|
||||||
|
modelUpdateVersions: `/api/lm/${modelType}/updates/versions`,
|
||||||
|
ignoreModelUpdate: `/api/lm/${modelType}/updates/ignore`,
|
||||||
|
ignoreVersionUpdate: `/api/lm/${modelType}/updates/ignore-version`,
|
||||||
|
|
||||||
// Preview management
|
// Preview management
|
||||||
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
||||||
|
|||||||
@@ -592,6 +592,73 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchModelUpdateVersions(modelId, { refresh = false, force = false } = {}) {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (refresh) params.append('refresh', 'true');
|
||||||
|
if (force) params.append('force', 'true');
|
||||||
|
const query = params.toString();
|
||||||
|
const requestUrl = `${this.apiConfig.endpoints.modelUpdateVersions}/${modelId}${query ? `?${query}` : ''}`;
|
||||||
|
|
||||||
|
const response = await fetch(requestUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || 'Failed to fetch model versions');
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching model update versions:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setModelUpdateIgnore(modelId, shouldIgnore) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.ignoreModelUpdate, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
modelId,
|
||||||
|
shouldIgnore,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || 'Failed to update model ignore status');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating model ignore status:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVersionUpdateIgnore(modelId, versionId, shouldIgnore) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.ignoreVersionUpdate, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
modelId,
|
||||||
|
versionId,
|
||||||
|
shouldIgnore,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || 'Failed to update version ignore status');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating version ignore status:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchModelRoots() {
|
async fetchModelRoots() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.apiConfig.endpoints.roots);
|
const response = await fetch(this.apiConfig.endpoints.roots);
|
||||||
@@ -1162,4 +1229,4 @@ export class BaseModelApiClient {
|
|||||||
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { translate } from '../../utils/i18nHelpers.js';
|
|||||||
/**
|
/**
|
||||||
* Set up tab switching functionality
|
* Set up tab switching functionality
|
||||||
*/
|
*/
|
||||||
export function setupTabSwitching() {
|
export function setupTabSwitching(options = {}) {
|
||||||
|
const { onTabChange } = options;
|
||||||
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
||||||
|
|
||||||
tabButtons.forEach(button => {
|
tabButtons.forEach(button => {
|
||||||
@@ -31,6 +32,14 @@ export function setupTabSwitching() {
|
|||||||
if (button.dataset.tab === 'description') {
|
if (button.dataset.tab === 'description') {
|
||||||
await loadModelDescription();
|
await loadModelDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof onTabChange === 'function') {
|
||||||
|
try {
|
||||||
|
await onTabChange(button.dataset.tab);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling tab change:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -176,4 +185,4 @@ export async function setupModelDescriptionEditing(filePath) {
|
|||||||
descContainer.classList.remove('editing');
|
descContainer.classList.remove('editing');
|
||||||
editBtn.classList.remove('visible');
|
editBtn.classList.remove('visible');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { getModelApiClient } from '../../api/modelApiFactory.js';
|
|||||||
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
|
||||||
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
||||||
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
||||||
|
import { initVersionsTab } from './ModelVersionsTab.js';
|
||||||
import { loadRecipesForLora } from './RecipeTab.js';
|
import { loadRecipesForLora } from './RecipeTab.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
|
|
||||||
@@ -65,19 +66,26 @@ export async function showModelModal(model, modelType) {
|
|||||||
const examplesText = translate('modals.model.tabs.examples', {}, 'Examples');
|
const examplesText = translate('modals.model.tabs.examples', {}, 'Examples');
|
||||||
const descriptionText = translate('modals.model.tabs.description', {}, 'Model Description');
|
const descriptionText = translate('modals.model.tabs.description', {}, 'Model Description');
|
||||||
const recipesText = translate('modals.model.tabs.recipes', {}, 'Recipes');
|
const recipesText = translate('modals.model.tabs.recipes', {}, 'Recipes');
|
||||||
|
const versionsText = translate('modals.model.tabs.versions', {}, 'Versions');
|
||||||
|
|
||||||
const tabsContent = modelType === 'loras' ?
|
const tabsContent = modelType === 'loras' ?
|
||||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
||||||
<button class="tab-btn" data-tab="recipes">${recipesText}</button>` :
|
<button class="tab-btn" data-tab="recipes">${recipesText}</button>
|
||||||
|
<button class="tab-btn" data-tab="versions">${versionsText}</button>` :
|
||||||
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
|
||||||
<button class="tab-btn" data-tab="description">${descriptionText}</button>`;
|
<button class="tab-btn" data-tab="description">${descriptionText}</button>
|
||||||
|
<button class="tab-btn" data-tab="versions">${versionsText}</button>`;
|
||||||
|
|
||||||
const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...');
|
const loadingExampleImagesText = translate('modals.model.loading.exampleImages', {}, 'Loading example images...');
|
||||||
const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...');
|
const loadingDescriptionText = translate('modals.model.loading.description', {}, 'Loading model description...');
|
||||||
const loadingRecipesText = translate('modals.model.loading.recipes', {}, 'Loading recipes...');
|
const loadingRecipesText = translate('modals.model.loading.recipes', {}, 'Loading recipes...');
|
||||||
const loadingExamplesText = translate('modals.model.loading.examples', {}, 'Loading examples...');
|
const loadingExamplesText = translate('modals.model.loading.examples', {}, 'Loading examples...');
|
||||||
|
|
||||||
|
const loadingVersionsText = translate('modals.model.loading.versions', {}, 'Loading versions...');
|
||||||
|
const civitaiModelId = modelWithFullData.civitai?.modelId || '';
|
||||||
|
const civitaiVersionId = modelWithFullData.civitai?.id || '';
|
||||||
|
|
||||||
const tabPanesContent = modelType === 'loras' ?
|
const tabPanesContent = modelType === 'loras' ?
|
||||||
`<div id="showcase-tab" class="tab-pane active">
|
`<div id="showcase-tab" class="tab-pane active">
|
||||||
<div class="example-images-loading">
|
<div class="example-images-loading">
|
||||||
@@ -99,6 +107,14 @@ export async function showModelModal(model, modelType) {
|
|||||||
<div class="recipes-loading">
|
<div class="recipes-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i> ${loadingRecipesText}
|
<i class="fas fa-spinner fa-spin"></i> ${loadingRecipesText}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="versions-tab" class="tab-pane">
|
||||||
|
<div class="model-versions-tab" data-model-id="${civitaiModelId}" data-model-type="${modelType}" data-current-version-id="${civitaiVersionId}">
|
||||||
|
<div class="versions-loading-state">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> ${loadingVersionsText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>` :
|
</div>` :
|
||||||
`<div id="showcase-tab" class="tab-pane active">
|
`<div id="showcase-tab" class="tab-pane active">
|
||||||
<div class="recipes-loading">
|
<div class="recipes-loading">
|
||||||
@@ -114,6 +130,14 @@ export async function showModelModal(model, modelType) {
|
|||||||
<div class="model-description-content hidden">
|
<div class="model-description-content hidden">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="versions-tab" class="tab-pane">
|
||||||
|
<div class="model-versions-tab" data-model-id="${civitaiModelId}" data-model-type="${modelType}" data-current-version-id="${civitaiVersionId}">
|
||||||
|
<div class="versions-loading-state">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> ${loadingVersionsText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const content = `
|
const content = `
|
||||||
@@ -232,9 +256,22 @@ export async function showModelModal(model, modelType) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
modalManager.showModal(modalId, content, null, onCloseCallback);
|
modalManager.showModal(modalId, content, null, onCloseCallback);
|
||||||
|
const versionsTabController = initVersionsTab({
|
||||||
|
modalId,
|
||||||
|
modelType,
|
||||||
|
modelId: civitaiModelId,
|
||||||
|
currentVersionId: civitaiVersionId,
|
||||||
|
});
|
||||||
setupEditableFields(modelWithFullData.file_path, modelType);
|
setupEditableFields(modelWithFullData.file_path, modelType);
|
||||||
setupShowcaseScroll(modalId);
|
setupShowcaseScroll(modalId);
|
||||||
setupTabSwitching();
|
setupTabSwitching({
|
||||||
|
onTabChange: async (tab) => {
|
||||||
|
if (tab === 'versions') {
|
||||||
|
await versionsTabController.load();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
versionsTabController.load({ eager: true });
|
||||||
setupTagTooltip();
|
setupTagTooltip();
|
||||||
setupTagEditMode(modelType);
|
setupTagEditMode(modelType);
|
||||||
setupModelNameEditing(modelWithFullData.file_path);
|
setupModelNameEditing(modelWithFullData.file_path);
|
||||||
|
|||||||
563
static/js/components/shared/ModelVersionsTab.js
Normal file
563
static/js/components/shared/ModelVersionsTab.js
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
|
import { downloadManager } from '../../managers/DownloadManager.js';
|
||||||
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
import { formatFileSize } from './utils.js';
|
||||||
|
|
||||||
|
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
if (value == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVideoUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url, window.location.origin);
|
||||||
|
const pathname = parsed.pathname || '';
|
||||||
|
const extension = pathname.slice(pathname.lastIndexOf('.')).toLowerCase();
|
||||||
|
return VIDEO_EXTENSIONS.includes(extension);
|
||||||
|
} catch (error) {
|
||||||
|
const normalized = url.split('?')[0].toLowerCase();
|
||||||
|
return VIDEO_EXTENSIONS.some(ext => normalized.endsWith(ext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateLabel(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMetaMarkup(version) {
|
||||||
|
const segments = [];
|
||||||
|
if (version.baseModel) {
|
||||||
|
segments.push(
|
||||||
|
`<span class="version-meta-primary">${escapeHtml(version.baseModel)}</span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const releaseLabel = formatDateLabel(version.releasedAt);
|
||||||
|
if (releaseLabel) {
|
||||||
|
segments.push(escapeHtml(releaseLabel));
|
||||||
|
}
|
||||||
|
if (typeof version.sizeBytes === 'number' && version.sizeBytes > 0) {
|
||||||
|
segments.push(escapeHtml(formatFileSize(version.sizeBytes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!segments.length) {
|
||||||
|
return escapeHtml(
|
||||||
|
translate('modals.model.versions.labels.noDetails', {}, 'No additional details')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments
|
||||||
|
.map(segment => `<span class="version-meta-item">${segment}</span>`)
|
||||||
|
.join('<span class="version-meta-separator">•</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBadge(label, tone) {
|
||||||
|
return `<span class="version-badge version-badge-${tone}">${escapeHtml(label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoplaySetting() {
|
||||||
|
try {
|
||||||
|
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMediaMarkup(version) {
|
||||||
|
if (!version.previewUrl) {
|
||||||
|
const placeholderText = translate('modals.model.versions.media.placeholder', {}, 'No preview');
|
||||||
|
return `<div class="version-media version-media-placeholder">${escapeHtml(placeholderText)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideoUrl(version.previewUrl)) {
|
||||||
|
const autoplayOnHover = getAutoplaySetting();
|
||||||
|
return `
|
||||||
|
<div class="version-media">
|
||||||
|
<video
|
||||||
|
src="${escapeHtml(version.previewUrl)}"
|
||||||
|
${autoplayOnHover ? '' : 'controls'}
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsinline
|
||||||
|
preload="metadata"
|
||||||
|
data-autoplay-on-hover="${autoplayOnHover ? 'true' : 'false'}"
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="version-media">
|
||||||
|
<img src="${escapeHtml(version.previewUrl)}" alt="${escapeHtml(version.name || 'preview')}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRow(version, options) {
|
||||||
|
const { latestLibraryVersionId, currentVersionId } = options;
|
||||||
|
const isCurrent = currentVersionId && version.versionId === currentVersionId;
|
||||||
|
const isNewer =
|
||||||
|
typeof latestLibraryVersionId === 'number' &&
|
||||||
|
version.versionId > latestLibraryVersionId;
|
||||||
|
const badges = [];
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
badges.push(buildBadge(translate('modals.model.versions.badges.current', {}, 'Current Version'), 'current'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.isInLibrary) {
|
||||||
|
badges.push(buildBadge(translate('modals.model.versions.badges.inLibrary', {}, 'In Library'), 'success'));
|
||||||
|
} else if (isNewer && !version.shouldIgnore) {
|
||||||
|
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.shouldIgnore) {
|
||||||
|
badges.push(buildBadge(translate('modals.model.versions.badges.ignored', {}, 'Ignored'), 'muted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadLabel = translate('modals.model.versions.actions.download', {}, 'Download');
|
||||||
|
const deleteLabel = translate('modals.model.versions.actions.delete', {}, 'Delete');
|
||||||
|
const ignoreLabel = translate(
|
||||||
|
version.shouldIgnore
|
||||||
|
? 'modals.model.versions.actions.unignore'
|
||||||
|
: 'modals.model.versions.actions.ignore',
|
||||||
|
{},
|
||||||
|
version.shouldIgnore ? 'Unignore' : 'Ignore'
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
if (!version.isInLibrary) {
|
||||||
|
actions.push(
|
||||||
|
`<button class="version-action version-action-primary" data-version-action="download">${escapeHtml(downloadLabel)}</button>`
|
||||||
|
);
|
||||||
|
} else if (version.filePath) {
|
||||||
|
actions.push(
|
||||||
|
`<button class="version-action version-action-danger" data-version-action="delete">${escapeHtml(deleteLabel)}</button>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
actions.push(
|
||||||
|
`<button class="version-action version-action-ghost" data-version-action="toggle-ignore" data-ignore-state="${
|
||||||
|
version.shouldIgnore ? 'ignored' : 'active'
|
||||||
|
}">${escapeHtml(ignoreLabel)}</button>`
|
||||||
|
);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="model-version-row${isCurrent ? ' is-current' : ''}" data-version-id="${escapeHtml(version.versionId)}">
|
||||||
|
${renderMediaMarkup(version)}
|
||||||
|
<div class="version-details">
|
||||||
|
<div class="version-title">
|
||||||
|
<span class="version-name">${escapeHtml(version.name || translate('modals.model.versions.labels.unnamed', {}, 'Untitled Version'))}</span>
|
||||||
|
<span class="version-id">#${escapeHtml(version.versionId)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="version-badges">${badges.join('')}</div>
|
||||||
|
<div class="version-meta">
|
||||||
|
${buildMetaMarkup(version)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="version-actions">
|
||||||
|
${actions.join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMediaHoverInteractions(container) {
|
||||||
|
const autoplayOnHover = getAutoplaySetting();
|
||||||
|
if (!autoplayOnHover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.querySelectorAll('.version-media video').forEach(video => {
|
||||||
|
if (video.dataset.autoplayOnHover !== 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const play = () => {
|
||||||
|
try {
|
||||||
|
video.currentTime = 0;
|
||||||
|
const promise = video.play();
|
||||||
|
if (promise && typeof promise.catch === 'function') {
|
||||||
|
promise.catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('Failed to autoplay preview video:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const stop = () => {
|
||||||
|
video.pause();
|
||||||
|
video.currentTime = 0;
|
||||||
|
};
|
||||||
|
video.addEventListener('mouseenter', play);
|
||||||
|
video.addEventListener('focus', play);
|
||||||
|
video.addEventListener('mouseleave', stop);
|
||||||
|
video.addEventListener('blur', stop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestLibraryVersionId(record) {
|
||||||
|
if (!record || !Array.isArray(record.inLibraryVersionIds) || !record.inLibraryVersionIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.max(...record.inLibraryVersionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToolbar(record) {
|
||||||
|
const ignoreText = record.shouldIgnore
|
||||||
|
? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model')
|
||||||
|
: translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model');
|
||||||
|
const viewLocalText = translate('modals.model.versions.actions.viewLocalVersions', {}, 'View all local versions');
|
||||||
|
const infoText = translate(
|
||||||
|
'modals.model.versions.copy',
|
||||||
|
{ count: record.versions.length },
|
||||||
|
'Track and manage every version of this model in one place.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<header class="versions-toolbar">
|
||||||
|
<div class="versions-toolbar-info">
|
||||||
|
<h3>${translate('modals.model.versions.heading', {}, 'Model versions')}</h3>
|
||||||
|
<p>${escapeHtml(infoText)}</p>
|
||||||
|
</div>
|
||||||
|
<div class="versions-toolbar-actions">
|
||||||
|
<button class="versions-toolbar-btn versions-toolbar-btn-primary" data-versions-action="toggle-model-ignore">
|
||||||
|
${escapeHtml(ignoreText)}
|
||||||
|
</button>
|
||||||
|
<button class="versions-toolbar-btn versions-toolbar-btn-secondary" data-versions-action="view-local" title="${escapeHtml(translate('modals.model.versions.actions.viewLocalTooltip', {}, 'Coming soon'))}" disabled>
|
||||||
|
${escapeHtml(viewLocalText)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmptyState(container) {
|
||||||
|
const message = translate('modals.model.versions.empty', {}, 'No version history available for this model yet.');
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="versions-empty">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>${escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderErrorState(container, message) {
|
||||||
|
const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.');
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="versions-error">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<p>${escapeHtml(message || fallback)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initVersionsTab({
|
||||||
|
modalId,
|
||||||
|
modelType,
|
||||||
|
modelId,
|
||||||
|
currentVersionId,
|
||||||
|
}) {
|
||||||
|
const pane = document.querySelector(`#${modalId} #versions-tab`);
|
||||||
|
const container = pane ? pane.querySelector('.model-versions-tab') : null;
|
||||||
|
const normalizedCurrentVersionId =
|
||||||
|
typeof currentVersionId === 'number'
|
||||||
|
? currentVersionId
|
||||||
|
: currentVersionId
|
||||||
|
? Number(currentVersionId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return {
|
||||||
|
async load() {},
|
||||||
|
async refresh() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller = {
|
||||||
|
isLoading: false,
|
||||||
|
hasLoaded: false,
|
||||||
|
record: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let apiClient;
|
||||||
|
|
||||||
|
function ensureClient() {
|
||||||
|
if (apiClient) {
|
||||||
|
return apiClient;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
apiClient = getModelApiClient(modelType);
|
||||||
|
} catch (error) {
|
||||||
|
apiClient = getModelApiClient();
|
||||||
|
}
|
||||||
|
return apiClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
const loadingText = translate('modals.model.loading.versions', {}, 'Loading versions...');
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="versions-loading-state">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i> ${escapeHtml(loadingText)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(record) {
|
||||||
|
controller.record = record;
|
||||||
|
controller.hasLoaded = true;
|
||||||
|
|
||||||
|
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
||||||
|
renderEmptyState(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
||||||
|
let dividerInserted = false;
|
||||||
|
|
||||||
|
const sortedVersions = [...record.versions].sort(
|
||||||
|
(a, b) => Number(b.versionId) - Number(a.versionId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rowsMarkup = sortedVersions
|
||||||
|
.map(version => {
|
||||||
|
const isNewer =
|
||||||
|
typeof latestLibraryVersionId === 'number' &&
|
||||||
|
version.versionId > latestLibraryVersionId;
|
||||||
|
let markup = '';
|
||||||
|
if (
|
||||||
|
!dividerInserted &&
|
||||||
|
typeof latestLibraryVersionId === 'number' &&
|
||||||
|
!isNewer
|
||||||
|
) {
|
||||||
|
dividerInserted = true;
|
||||||
|
markup += '<div class="version-divider" role="presentation"></div>';
|
||||||
|
}
|
||||||
|
markup += renderRow(version, {
|
||||||
|
latestLibraryVersionId,
|
||||||
|
currentVersionId: normalizedCurrentVersionId,
|
||||||
|
});
|
||||||
|
return markup;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
${renderToolbar(record)}
|
||||||
|
<div class="versions-list">
|
||||||
|
${rowsMarkup}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setupMediaHoverInteractions(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
||||||
|
if (controller.isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!modelId) {
|
||||||
|
renderErrorState(container, translate('modals.model.versions.missingModelId', {}, 'This model is missing a Civitai model id.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (controller.hasLoaded && !forceRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.isLoading = true;
|
||||||
|
if (!eager) {
|
||||||
|
showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = ensureClient();
|
||||||
|
const response = await client.fetchModelUpdateVersions(modelId, {
|
||||||
|
refresh: true,
|
||||||
|
});
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error(response?.error || 'Request failed');
|
||||||
|
}
|
||||||
|
render(response.record);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load model versions:', error);
|
||||||
|
renderErrorState(container, error?.message);
|
||||||
|
} finally {
|
||||||
|
controller.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await loadVersions({ forceRefresh: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleModelIgnore(button) {
|
||||||
|
if (!controller.record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = ensureClient();
|
||||||
|
const nextValue = !controller.record.shouldIgnore;
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const response = await client.setModelUpdateIgnore(modelId, nextValue);
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error(response?.error || 'Request failed');
|
||||||
|
}
|
||||||
|
render(response.record);
|
||||||
|
const toastKey = nextValue
|
||||||
|
? 'modals.model.versions.toast.modelIgnored'
|
||||||
|
: 'modals.model.versions.toast.modelResumed';
|
||||||
|
const toastMessage = translate(
|
||||||
|
toastKey,
|
||||||
|
{},
|
||||||
|
nextValue ? 'Updates ignored for this model' : 'Update tracking resumed'
|
||||||
|
);
|
||||||
|
showToast(toastMessage, {}, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update model ignore state:', error);
|
||||||
|
showToast(error?.message || 'Failed to update ignore preference', {}, 'error');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleVersionIgnore(button, versionId) {
|
||||||
|
if (!controller.record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = ensureClient();
|
||||||
|
const targetVersion = controller.record.versions.find(v => v.versionId === versionId);
|
||||||
|
const nextValue = targetVersion ? !targetVersion.shouldIgnore : true;
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const response = await client.setVersionUpdateIgnore(
|
||||||
|
modelId,
|
||||||
|
versionId,
|
||||||
|
nextValue
|
||||||
|
);
|
||||||
|
if (!response?.success) {
|
||||||
|
throw new Error(response?.error || 'Request failed');
|
||||||
|
}
|
||||||
|
render(response.record);
|
||||||
|
const updatedVersion = response.record.versions.find(v => v.versionId === versionId);
|
||||||
|
const toastKey = updatedVersion?.shouldIgnore
|
||||||
|
? 'modals.model.versions.toast.versionIgnored'
|
||||||
|
: 'modals.model.versions.toast.versionUnignored';
|
||||||
|
const toastMessage = translate(
|
||||||
|
toastKey,
|
||||||
|
{},
|
||||||
|
updatedVersion?.shouldIgnore ? 'Updates ignored for this version' : 'Version re-enabled'
|
||||||
|
);
|
||||||
|
showToast(toastMessage, {}, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle version ignore state:', error);
|
||||||
|
showToast(error?.message || 'Failed to update version preference', {}, 'error');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteVersion(button, versionId) {
|
||||||
|
if (!controller.record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const version = controller.record.versions.find(item => item.versionId === versionId);
|
||||||
|
if (!version?.filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const confirmText = translate(
|
||||||
|
'modals.model.versions.confirm.delete',
|
||||||
|
{},
|
||||||
|
'Delete this version from your library?'
|
||||||
|
);
|
||||||
|
if (!window.confirm(confirmText)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
const client = ensureClient();
|
||||||
|
await client.deleteModel(version.filePath);
|
||||||
|
showToast(
|
||||||
|
translate('modals.model.versions.toast.versionDeleted', {}, 'Version deleted'),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete version:', error);
|
||||||
|
showToast(error?.message || 'Failed to delete version', {}, 'error');
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadVersion(versionId) {
|
||||||
|
if (!controller.record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadManager.openForModelVersion(modelType, modelId, versionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener('click', async event => {
|
||||||
|
const toolbarAction = event.target.closest('[data-versions-action]');
|
||||||
|
if (toolbarAction) {
|
||||||
|
const action = toolbarAction.dataset.versionsAction;
|
||||||
|
if (action === 'toggle-model-ignore') {
|
||||||
|
event.preventDefault();
|
||||||
|
await handleToggleModelIgnore(toolbarAction);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionButton = event.target.closest('[data-version-action]');
|
||||||
|
if (!actionButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = actionButton.closest('.model-version-row');
|
||||||
|
if (!row) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const versionId = Number(row.dataset.versionId);
|
||||||
|
const action = actionButton.dataset.versionAction;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'download':
|
||||||
|
event.preventDefault();
|
||||||
|
handleDownloadVersion(versionId);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
event.preventDefault();
|
||||||
|
await handleDeleteVersion(actionButton, versionId);
|
||||||
|
break;
|
||||||
|
case 'toggle-ignore':
|
||||||
|
event.preventDefault();
|
||||||
|
await handleToggleVersionIgnore(actionButton, versionId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
load: options => loadVersions(options),
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -140,6 +140,14 @@ export class DownloadManager {
|
|||||||
this.loadDefaultPathSetting();
|
this.loadDefaultPathSetting();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async retrieveVersionsForModel(modelId, source = null) {
|
||||||
|
this.versions = await this.apiClient.fetchCivitaiVersions(modelId, source);
|
||||||
|
if (!this.versions || !this.versions.length) {
|
||||||
|
throw new Error(translate('modals.download.errors.noVersions'));
|
||||||
|
}
|
||||||
|
return this.versions;
|
||||||
|
}
|
||||||
|
|
||||||
async validateAndFetchVersions() {
|
async validateAndFetchVersions() {
|
||||||
const url = document.getElementById('modelUrl').value.trim();
|
const url = document.getElementById('modelUrl').value.trim();
|
||||||
const errorElement = document.getElementById('urlError');
|
const errorElement = document.getElementById('urlError');
|
||||||
@@ -152,12 +160,8 @@ export class DownloadManager {
|
|||||||
throw new Error(translate('modals.download.errors.invalidUrl'));
|
throw new Error(translate('modals.download.errors.invalidUrl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.versions = await this.apiClient.fetchCivitaiVersions(this.modelId, this.source);
|
await this.retrieveVersionsForModel(this.modelId, this.source);
|
||||||
|
|
||||||
if (!this.versions.length) {
|
|
||||||
throw new Error(translate('modals.download.errors.noVersions'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a version ID from URL, pre-select it
|
// If we have a version ID from URL, pre-select it
|
||||||
if (this.modelVersionId) {
|
if (this.modelVersionId) {
|
||||||
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
|
this.currentVersion = this.versions.find(v => v.id.toString() === this.modelVersionId);
|
||||||
@@ -171,6 +175,27 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchVersionsForCurrentModel() {
|
||||||
|
const errorElement = document.getElementById('urlError');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.loadingManager.showSimpleLoading(translate('modals.download.fetchingVersions'));
|
||||||
|
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) {
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = error.message;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extractModelId(url) {
|
extractModelId(url) {
|
||||||
const versionMatch = url.match(/modelVersionId=(\d+)/i);
|
const versionMatch = url.match(/modelVersionId=(\d+)/i);
|
||||||
this.modelVersionId = versionMatch ? versionMatch[1] : null;
|
this.modelVersionId = versionMatch ? versionMatch[1] : null;
|
||||||
@@ -191,6 +216,26 @@ export class DownloadManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openForModelVersion(modelType, modelId, versionId = null) {
|
||||||
|
try {
|
||||||
|
this.apiClient = getModelApiClient(modelType);
|
||||||
|
} catch (error) {
|
||||||
|
this.apiClient = getModelApiClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showDownloadModal();
|
||||||
|
|
||||||
|
this.modelId = modelId ? modelId.toString() : null;
|
||||||
|
this.modelVersionId = versionId ? versionId.toString() : null;
|
||||||
|
this.source = null;
|
||||||
|
|
||||||
|
if (!this.modelId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fetchVersionsForCurrentModel();
|
||||||
|
}
|
||||||
|
|
||||||
showVersionStep() {
|
showVersionStep() {
|
||||||
document.getElementById('urlStep').style.display = 'none';
|
document.getElementById('urlStep').style.display = 'none';
|
||||||
document.getElementById('versionStep').style.display = 'block';
|
document.getElementById('versionStep').style.display = 'block';
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class DummyUpdateService:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_build_preview_overrides_uses_static_urls():
|
async def test_build_version_context_includes_static_urls():
|
||||||
cache = SimpleNamespace(version_index={123: {"preview_url": "/tmp/previews/example.png"}})
|
cache = SimpleNamespace(version_index={123: {"preview_url": "/tmp/previews/example.png"}})
|
||||||
service = DummyService(cache)
|
service = DummyService(cache)
|
||||||
handler = ModelUpdateHandler(
|
handler = ModelUpdateHandler(
|
||||||
@@ -70,9 +70,9 @@ async def test_build_preview_overrides_uses_static_urls():
|
|||||||
should_ignore_model=False,
|
should_ignore_model=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides = await handler._build_preview_overrides(record)
|
overrides = await handler._build_version_context(record)
|
||||||
expected = config.get_preview_static_url("/tmp/previews/example.png")
|
expected = config.get_preview_static_url("/tmp/previews/example.png")
|
||||||
assert overrides == {123: expected}
|
assert overrides == {123: {"file_path": None, "file_name": None, "preview_override": expected}}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user