Compare commits

...

3 Commits

Author SHA1 Message Date
Will Miao
6d0d9600a7 fix(versions): clarify tab hover states and copy 2026-04-13 21:12:13 +08:00
Will Miao
70cd3f4e1b fix(download-history): use title for downloaded tooltip 2026-04-13 20:26:40 +08:00
Will Miao
a95c518b30 feat(download-history): add downloaded status UX 2026-04-13 19:51:04 +08:00
18 changed files with 768 additions and 59 deletions

View File

@@ -957,6 +957,8 @@
"earlyAccess": "Early Access", "earlyAccess": "Early Access",
"earlyAccessTooltip": "Early Access erforderlich", "earlyAccessTooltip": "Early Access erforderlich",
"inLibrary": "In Bibliothek", "inLibrary": "In Bibliothek",
"downloaded": "Heruntergeladen",
"downloadedTooltip": "Zuvor heruntergeladen, aber derzeit nicht in Ihrer Bibliothek.",
"alreadyInLibrary": "Bereits in Bibliothek", "alreadyInLibrary": "Bereits in Bibliothek",
"autoOrganizedPath": "[Automatisch organisiert durch Pfadvorlage]", "autoOrganizedPath": "[Automatisch organisiert durch Pfadvorlage]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "in {count}d" "days": "in {count}d"
}, },
"badges": { "badges": {
"current": "Aktuelle Version", "current": "Geöffnete Version",
"currentTooltip": "Das ist die Version, mit der dieses Modal geöffnet wurde",
"inLibrary": "In der Bibliothek", "inLibrary": "In der Bibliothek",
"inLibraryTooltip": "Diese Version befindet sich in Ihrer lokalen Bibliothek",
"downloaded": "Heruntergeladen",
"downloadedTooltip": "Diese Version wurde bereits heruntergeladen, befindet sich aber derzeit nicht in Ihrer Bibliothek",
"newer": "Neuere Version", "newer": "Neuere Version",
"newerTooltip": "Diese Version ist neuer als Ihre neueste lokale Version",
"earlyAccess": "Früher Zugriff", "earlyAccess": "Früher Zugriff",
"ignored": "Ignoriert" "earlyAccessTooltip": "Für diese Version ist derzeit Civitai Early Access erforderlich",
"ignored": "Ignoriert",
"ignoredTooltip": "Für diese Version sind Update-Benachrichtigungen deaktiviert"
}, },
"actions": { "actions": {
"download": "Herunterladen", "download": "Herunterladen",
"downloadTooltip": "Diese Version herunterladen",
"downloadEarlyAccessTooltip": "Diese Early-Access-Version von Civitai herunterladen",
"delete": "Löschen", "delete": "Löschen",
"deleteTooltip": "Diese lokale Version löschen",
"ignore": "Ignorieren", "ignore": "Ignorieren",
"unignore": "Ignorierung aufheben", "unignore": "Ignorierung aufheben",
"ignoreTooltip": "Update-Benachrichtigungen für diese Version ignorieren",
"unignoreTooltip": "Update-Benachrichtigungen für diese Version fortsetzen",
"viewVersionOnCivitai": "Version auf Civitai anzeigen",
"earlyAccessTooltip": "Erfordert Early-Access-Kauf", "earlyAccessTooltip": "Erfordert Early-Access-Kauf",
"resumeModelUpdates": "Aktualisierungen für dieses Modell fortsetzen", "resumeModelUpdates": "Aktualisierungen für dieses Modell fortsetzen",
"ignoreModelUpdates": "Aktualisierungen für dieses Modell ignorieren", "ignoreModelUpdates": "Aktualisierungen für dieses Modell ignorieren",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "Early Access", "earlyAccess": "Early Access",
"earlyAccessTooltip": "Early access required", "earlyAccessTooltip": "Early access required",
"inLibrary": "In Library", "inLibrary": "In Library",
"downloaded": "Downloaded",
"downloadedTooltip": "Previously downloaded, but it is not currently in your library.",
"alreadyInLibrary": "Already in Library", "alreadyInLibrary": "Already in Library",
"autoOrganizedPath": "[Auto-organized by path template]", "autoOrganizedPath": "[Auto-organized by path template]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "in {count}d" "days": "in {count}d"
}, },
"badges": { "badges": {
"current": "Current Version", "current": "Opened Version",
"currentTooltip": "This is the version you opened this modal from",
"inLibrary": "In Library", "inLibrary": "In Library",
"inLibraryTooltip": "This version exists in your local library",
"downloaded": "Downloaded",
"downloadedTooltip": "This version was downloaded before, but is not currently in your library",
"newer": "Newer Version", "newer": "Newer Version",
"newerTooltip": "This version is newer than your latest local version",
"earlyAccess": "Early Access", "earlyAccess": "Early Access",
"ignored": "Ignored" "earlyAccessTooltip": "This version currently requires Civitai early access",
"ignored": "Ignored",
"ignoredTooltip": "Update notifications are disabled for this version"
}, },
"actions": { "actions": {
"download": "Download", "download": "Download",
"downloadTooltip": "Download this version",
"downloadEarlyAccessTooltip": "Download this early access version from Civitai",
"delete": "Delete", "delete": "Delete",
"deleteTooltip": "Delete this local version",
"ignore": "Ignore", "ignore": "Ignore",
"unignore": "Unignore", "unignore": "Unignore",
"ignoreTooltip": "Ignore update notifications for this version",
"unignoreTooltip": "Resume update notifications for this version",
"viewVersionOnCivitai": "View version on Civitai",
"earlyAccessTooltip": "Requires early access purchase", "earlyAccessTooltip": "Requires early access purchase",
"resumeModelUpdates": "Resume updates for this model", "resumeModelUpdates": "Resume updates for this model",
"ignoreModelUpdates": "Ignore updates for this model", "ignoreModelUpdates": "Ignore updates for this model",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "Acceso temprano", "earlyAccess": "Acceso temprano",
"earlyAccessTooltip": "Acceso temprano requerido", "earlyAccessTooltip": "Acceso temprano requerido",
"inLibrary": "En la biblioteca", "inLibrary": "En la biblioteca",
"downloaded": "Descargado",
"downloadedTooltip": "Descargado anteriormente, pero actualmente no está en tu biblioteca.",
"alreadyInLibrary": "Ya en la biblioteca", "alreadyInLibrary": "Ya en la biblioteca",
"autoOrganizedPath": "[Auto-organizado por plantilla de ruta]", "autoOrganizedPath": "[Auto-organizado por plantilla de ruta]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "en {count}d" "days": "en {count}d"
}, },
"badges": { "badges": {
"current": "Versión actual", "current": "Versión abierta",
"currentTooltip": "Es la versión con la que abriste este modal",
"inLibrary": "En la biblioteca", "inLibrary": "En la biblioteca",
"inLibraryTooltip": "Esta versión existe en tu biblioteca local",
"downloaded": "Descargado",
"downloadedTooltip": "Esta versión se descargó antes, pero ahora no está en tu biblioteca",
"newer": "Versión más reciente", "newer": "Versión más reciente",
"newerTooltip": "Esta versión es más reciente que tu última versión local",
"earlyAccess": "Acceso temprano", "earlyAccess": "Acceso temprano",
"ignored": "Ignorada" "earlyAccessTooltip": "Esta versión requiere actualmente acceso temprano de Civitai",
"ignored": "Ignorada",
"ignoredTooltip": "Las notificaciones de actualización están desactivadas para esta versión"
}, },
"actions": { "actions": {
"download": "Descargar", "download": "Descargar",
"downloadTooltip": "Descargar esta versión",
"downloadEarlyAccessTooltip": "Descargar esta versión de acceso temprano desde Civitai",
"delete": "Eliminar", "delete": "Eliminar",
"deleteTooltip": "Eliminar esta versión local",
"ignore": "Ignorar", "ignore": "Ignorar",
"unignore": "Dejar de ignorar", "unignore": "Dejar de ignorar",
"ignoreTooltip": "Ignorar las notificaciones de actualización de esta versión",
"unignoreTooltip": "Reanudar las notificaciones de actualización de esta versión",
"viewVersionOnCivitai": "Ver versión en Civitai",
"earlyAccessTooltip": "Requiere compra de acceso temprano", "earlyAccessTooltip": "Requiere compra de acceso temprano",
"resumeModelUpdates": "Reanudar actualizaciones para este modelo", "resumeModelUpdates": "Reanudar actualizaciones para este modelo",
"ignoreModelUpdates": "Ignorar actualizaciones para este modelo", "ignoreModelUpdates": "Ignorar actualizaciones para este modelo",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "Accès anticipé", "earlyAccess": "Accès anticipé",
"earlyAccessTooltip": "Accès anticipé requis", "earlyAccessTooltip": "Accès anticipé requis",
"inLibrary": "Dans la bibliothèque", "inLibrary": "Dans la bibliothèque",
"downloaded": "Téléchargé",
"downloadedTooltip": "Déjà téléchargé, mais il n'est actuellement pas dans votre bibliothèque.",
"alreadyInLibrary": "Déjà dans la bibliothèque", "alreadyInLibrary": "Déjà dans la bibliothèque",
"autoOrganizedPath": "[Auto-organisé par modèle de chemin]", "autoOrganizedPath": "[Auto-organisé par modèle de chemin]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "dans {count}j" "days": "dans {count}j"
}, },
"badges": { "badges": {
"current": "Version actuelle", "current": "Version ouverte",
"currentTooltip": "C'est la version à partir de laquelle cette fenêtre a été ouverte",
"inLibrary": "Dans la bibliothèque", "inLibrary": "Dans la bibliothèque",
"inLibraryTooltip": "Cette version existe dans votre bibliothèque locale",
"downloaded": "Téléchargé",
"downloadedTooltip": "Cette version a déjà été téléchargée, mais n'est pas actuellement dans votre bibliothèque",
"newer": "Version plus récente", "newer": "Version plus récente",
"newerTooltip": "Cette version est plus récente que votre dernière version locale",
"earlyAccess": "Accès anticipé", "earlyAccess": "Accès anticipé",
"ignored": "Ignorée" "earlyAccessTooltip": "Cette version nécessite actuellement l'accès anticipé Civitai",
"ignored": "Ignorée",
"ignoredTooltip": "Les notifications de mise à jour sont désactivées pour cette version"
}, },
"actions": { "actions": {
"download": "Télécharger", "download": "Télécharger",
"downloadTooltip": "Télécharger cette version",
"downloadEarlyAccessTooltip": "Télécharger cette version en accès anticipé depuis Civitai",
"delete": "Supprimer", "delete": "Supprimer",
"deleteTooltip": "Supprimer cette version locale",
"ignore": "Ignorer", "ignore": "Ignorer",
"unignore": "Ne plus ignorer", "unignore": "Ne plus ignorer",
"ignoreTooltip": "Ignorer les notifications de mise à jour pour cette version",
"unignoreTooltip": "Reprendre les notifications de mise à jour pour cette version",
"viewVersionOnCivitai": "Voir la version sur Civitai",
"earlyAccessTooltip": "Nécessite l'achat de l'accès anticipé", "earlyAccessTooltip": "Nécessite l'achat de l'accès anticipé",
"resumeModelUpdates": "Reprendre les mises à jour pour ce modèle", "resumeModelUpdates": "Reprendre les mises à jour pour ce modèle",
"ignoreModelUpdates": "Ignorer les mises à jour pour ce modèle", "ignoreModelUpdates": "Ignorer les mises à jour pour ce modèle",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "גישה מוקדמת", "earlyAccess": "גישה מוקדמת",
"earlyAccessTooltip": "נדרשת גישה מוקדמת", "earlyAccessTooltip": "נדרשת גישה מוקדמת",
"inLibrary": "בספרייה", "inLibrary": "בספרייה",
"downloaded": "הורד",
"downloadedTooltip": "הורד בעבר, אך הוא אינו נמצא כרגע בספרייה שלך.",
"alreadyInLibrary": "כבר בספרייה", "alreadyInLibrary": "כבר בספרייה",
"autoOrganizedPath": "[מאורגן אוטומטית לפי תבנית נתיב]", "autoOrganizedPath": "[מאורגן אוטומטית לפי תבנית נתיב]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "בעוד {count} ימים" "days": "בעוד {count} ימים"
}, },
"badges": { "badges": {
"current": "גרסה נוכחית", "current": "גרסה שנפתחה",
"currentTooltip": "זוהי הגרסה שממנה נפתח החלון הזה",
"inLibrary": "בספרייה", "inLibrary": "בספרייה",
"inLibraryTooltip": "גרסה זו קיימת בספרייה המקומית שלך",
"downloaded": "הורד",
"downloadedTooltip": "גרסה זו הורדה בעבר, אך אינה נמצאת כרגע בספרייה שלך",
"newer": "גרסה חדשה יותר", "newer": "גרסה חדשה יותר",
"newerTooltip": "גרסה זו חדשה יותר מהגרסה המקומית האחרונה שלך",
"earlyAccess": "גישה מוקדמת", "earlyAccess": "גישה מוקדמת",
"ignored": "התעלם" "earlyAccessTooltip": "גרסה זו דורשת כרגע גישת Early Access של Civitai",
"ignored": "התעלם",
"ignoredTooltip": "התראות העדכון מושבתות עבור גרסה זו"
}, },
"actions": { "actions": {
"download": "הורדה", "download": "הורדה",
"downloadTooltip": "הורד את הגרסה הזו",
"downloadEarlyAccessTooltip": "הורד את גרסת ה-Early Access הזו מ-Civitai",
"delete": "מחיקה", "delete": "מחיקה",
"deleteTooltip": "מחק את הגרסה המקומית הזו",
"ignore": "התעלם", "ignore": "התעלם",
"unignore": "בטל התעלמות", "unignore": "בטל התעלמות",
"ignoreTooltip": "התעלם מהתראות העדכון עבור גרסה זו",
"unignoreTooltip": "חזור לקבל התראות עדכון עבור גרסה זו",
"viewVersionOnCivitai": "הצג את הגרסה ב-Civitai",
"earlyAccessTooltip": "נדרש רכישת גישה מוקדמת", "earlyAccessTooltip": "נדרש רכישת גישה מוקדמת",
"resumeModelUpdates": "המשך עדכונים עבור מודל זה", "resumeModelUpdates": "המשך עדכונים עבור מודל זה",
"ignoreModelUpdates": "התעלם מעדכונים עבור מודל זה", "ignoreModelUpdates": "התעלם מעדכונים עבור מודל זה",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "アーリーアクセス", "earlyAccess": "アーリーアクセス",
"earlyAccessTooltip": "アーリーアクセスが必要", "earlyAccessTooltip": "アーリーアクセスが必要",
"inLibrary": "ライブラリ内", "inLibrary": "ライブラリ内",
"downloaded": "ダウンロード済み",
"downloadedTooltip": "以前にダウンロード済みですが、現在はライブラリにありません。",
"alreadyInLibrary": "既にライブラリ内", "alreadyInLibrary": "既にライブラリ内",
"autoOrganizedPath": "[パステンプレートによる自動整理]", "autoOrganizedPath": "[パステンプレートによる自動整理]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "{count}日後" "days": "{count}日後"
}, },
"badges": { "badges": {
"current": "現在のバージョン", "current": "開いたバージョン",
"currentTooltip": "このモーダルを開くために選択したバージョンです",
"inLibrary": "ライブラリにあります", "inLibrary": "ライブラリにあります",
"inLibraryTooltip": "このバージョンはローカルライブラリに存在します",
"downloaded": "ダウンロード済み",
"downloadedTooltip": "このバージョンは以前ダウンロードされましたが、現在はライブラリにありません",
"newer": "新しいバージョン", "newer": "新しいバージョン",
"newerTooltip": "このバージョンはローカルの最新バージョンより新しいです",
"earlyAccess": "早期アクセス", "earlyAccess": "早期アクセス",
"ignored": "無視中" "earlyAccessTooltip": "このバージョンは現在 Civitai の早期アクセスが必要です",
"ignored": "無視中",
"ignoredTooltip": "このバージョンの更新通知は無効です"
}, },
"actions": { "actions": {
"download": "ダウンロード", "download": "ダウンロード",
"downloadTooltip": "このバージョンをダウンロード",
"downloadEarlyAccessTooltip": "Civitai からこの早期アクセス版をダウンロード",
"delete": "削除", "delete": "削除",
"deleteTooltip": "このローカルバージョンを削除",
"ignore": "無視", "ignore": "無視",
"unignore": "無視を解除", "unignore": "無視を解除",
"ignoreTooltip": "このバージョンの更新通知を無視",
"unignoreTooltip": "このバージョンの更新通知を再開",
"viewVersionOnCivitai": "Civitai でバージョンを表示",
"earlyAccessTooltip": "早期アクセス購入が必要", "earlyAccessTooltip": "早期アクセス購入が必要",
"resumeModelUpdates": "このモデルの更新を再開", "resumeModelUpdates": "このモデルの更新を再開",
"ignoreModelUpdates": "このモデルの更新を無視", "ignoreModelUpdates": "このモデルの更新を無視",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "얼리 액세스", "earlyAccess": "얼리 액세스",
"earlyAccessTooltip": "얼리 액세스 필요", "earlyAccessTooltip": "얼리 액세스 필요",
"inLibrary": "라이브러리에 있음", "inLibrary": "라이브러리에 있음",
"downloaded": "다운로드됨",
"downloadedTooltip": "이전에 다운로드했지만 현재 라이브러리에 없습니다.",
"alreadyInLibrary": "이미 라이브러리에 있음", "alreadyInLibrary": "이미 라이브러리에 있음",
"autoOrganizedPath": "[경로 템플릿으로 자동 정리됨]", "autoOrganizedPath": "[경로 템플릿으로 자동 정리됨]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "{count}일 후" "days": "{count}일 후"
}, },
"badges": { "badges": {
"current": "현재 버전", "current": "열린 버전",
"currentTooltip": "이 모달을 열 때 사용한 버전입니다",
"inLibrary": "라이브러리에 있음", "inLibrary": "라이브러리에 있음",
"inLibraryTooltip": "이 버전은 로컬 라이브러리에 있습니다",
"downloaded": "다운로드됨",
"downloadedTooltip": "이 버전은 이전에 다운로드되었지만 현재는 라이브러리에 없습니다",
"newer": "최신 버전", "newer": "최신 버전",
"newerTooltip": "이 버전은 로컬의 최신 버전보다 더 새롭습니다",
"earlyAccess": "얼리 액세스", "earlyAccess": "얼리 액세스",
"ignored": "무시됨" "earlyAccessTooltip": "이 버전은 현재 Civitai 얼리 액세스가 필요합니다",
"ignored": "무시됨",
"ignoredTooltip": "이 버전은 업데이트 알림이 비활성화되어 있습니다"
}, },
"actions": { "actions": {
"download": "다운로드", "download": "다운로드",
"downloadTooltip": "이 버전 다운로드",
"downloadEarlyAccessTooltip": "Civitai에서 이 얼리 액세스 버전 다운로드",
"delete": "삭제", "delete": "삭제",
"deleteTooltip": "이 로컬 버전 삭제",
"ignore": "무시", "ignore": "무시",
"unignore": "무시 해제", "unignore": "무시 해제",
"ignoreTooltip": "이 버전의 업데이트 알림 무시",
"unignoreTooltip": "이 버전의 업데이트 알림 다시 받기",
"viewVersionOnCivitai": "Civitai에서 버전 보기",
"earlyAccessTooltip": "얼리 액세스 구매 필요", "earlyAccessTooltip": "얼리 액세스 구매 필요",
"resumeModelUpdates": "이 모델 업데이트 재개", "resumeModelUpdates": "이 모델 업데이트 재개",
"ignoreModelUpdates": "이 모델 업데이트 무시", "ignoreModelUpdates": "이 모델 업데이트 무시",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "Ранний доступ", "earlyAccess": "Ранний доступ",
"earlyAccessTooltip": "Требуется ранний доступ", "earlyAccessTooltip": "Требуется ранний доступ",
"inLibrary": "В библиотеке", "inLibrary": "В библиотеке",
"downloaded": "Загружено",
"downloadedTooltip": "Ранее загружено, но сейчас этого нет в вашей библиотеке.",
"alreadyInLibrary": "Уже в библиотеке", "alreadyInLibrary": "Уже в библиотеке",
"autoOrganizedPath": "[Автоматически организовано по шаблону пути]", "autoOrganizedPath": "[Автоматически организовано по шаблону пути]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "через {count}д" "days": "через {count}д"
}, },
"badges": { "badges": {
"current": "Текущая версия", "current": "Открытая версия",
"currentTooltip": "Это версия, с которой было открыто это окно",
"inLibrary": "В библиотеке", "inLibrary": "В библиотеке",
"inLibraryTooltip": "Эта версия есть в вашей локальной библиотеке",
"downloaded": "Загружено",
"downloadedTooltip": "Эта версия уже загружалась, но сейчас отсутствует в вашей библиотеке",
"newer": "Более новая версия", "newer": "Более новая версия",
"newerTooltip": "Эта версия новее вашей последней локальной версии",
"earlyAccess": "Ранний доступ", "earlyAccess": "Ранний доступ",
"ignored": "Игнорируется" "earlyAccessTooltip": "Для этой версии сейчас требуется ранний доступ Civitai",
"ignored": "Игнорируется",
"ignoredTooltip": "Уведомления об обновлениях для этой версии отключены"
}, },
"actions": { "actions": {
"download": "Скачать", "download": "Скачать",
"downloadTooltip": "Скачать эту версию",
"downloadEarlyAccessTooltip": "Скачать эту версию раннего доступа с Civitai",
"delete": "Удалить", "delete": "Удалить",
"deleteTooltip": "Удалить эту локальную версию",
"ignore": "Игнорировать", "ignore": "Игнорировать",
"unignore": "Перестать игнорировать", "unignore": "Перестать игнорировать",
"ignoreTooltip": "Игнорировать уведомления об обновлениях для этой версии",
"unignoreTooltip": "Возобновить уведомления об обновлениях для этой версии",
"viewVersionOnCivitai": "Посмотреть версию на Civitai",
"earlyAccessTooltip": "Требуется покупка раннего доступа", "earlyAccessTooltip": "Требуется покупка раннего доступа",
"resumeModelUpdates": "Возобновить обновления для этой модели", "resumeModelUpdates": "Возобновить обновления для этой модели",
"ignoreModelUpdates": "Игнорировать обновления для этой модели", "ignoreModelUpdates": "Игнорировать обновления для этой модели",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "早期访问", "earlyAccess": "早期访问",
"earlyAccessTooltip": "需要早期访问权限", "earlyAccessTooltip": "需要早期访问权限",
"inLibrary": "已在库中", "inLibrary": "已在库中",
"downloaded": "已下载",
"downloadedTooltip": "之前已下载,但当前不在你的库中。",
"alreadyInLibrary": "已存在于库中", "alreadyInLibrary": "已存在于库中",
"autoOrganizedPath": "【已按路径模板自动整理】", "autoOrganizedPath": "【已按路径模板自动整理】",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "{count}天后" "days": "{count}天后"
}, },
"badges": { "badges": {
"current": "当前版本", "current": "已打开版本",
"currentTooltip": "这是你用来打开此弹窗的版本",
"inLibrary": "已在库中", "inLibrary": "已在库中",
"inLibraryTooltip": "此版本已存在于你的本地库中",
"downloaded": "已下载",
"downloadedTooltip": "此版本之前下载过,但当前不在你的本地库中",
"newer": "较新的版本", "newer": "较新的版本",
"newerTooltip": "此版本比你本地的最新版本更新",
"earlyAccess": "抢先体验", "earlyAccess": "抢先体验",
"ignored": "已忽略" "earlyAccessTooltip": "此版本当前需要 Civitai 抢先体验权限",
"ignored": "已忽略",
"ignoredTooltip": "此版本已关闭更新通知"
}, },
"actions": { "actions": {
"download": "下载", "download": "下载",
"downloadTooltip": "下载此版本",
"downloadEarlyAccessTooltip": "从 Civitai 下载此抢先体验版本",
"delete": "删除", "delete": "删除",
"deleteTooltip": "删除此本地版本",
"ignore": "忽略", "ignore": "忽略",
"unignore": "取消忽略", "unignore": "取消忽略",
"ignoreTooltip": "忽略此版本的更新通知",
"unignoreTooltip": "恢复此版本的更新通知",
"viewVersionOnCivitai": "在 Civitai 上查看版本",
"earlyAccessTooltip": "需要购买抢先体验", "earlyAccessTooltip": "需要购买抢先体验",
"resumeModelUpdates": "继续跟踪该模型的更新", "resumeModelUpdates": "继续跟踪该模型的更新",
"ignoreModelUpdates": "忽略该模型的更新", "ignoreModelUpdates": "忽略该模型的更新",

View File

@@ -957,6 +957,8 @@
"earlyAccess": "早期存取", "earlyAccess": "早期存取",
"earlyAccessTooltip": "需要早期存取", "earlyAccessTooltip": "需要早期存取",
"inLibrary": "已在庫存", "inLibrary": "已在庫存",
"downloaded": "已下載",
"downloadedTooltip": "先前已下載,但目前不在你的庫中。",
"alreadyInLibrary": "已在庫存", "alreadyInLibrary": "已在庫存",
"autoOrganizedPath": "[依路徑範本自動整理]", "autoOrganizedPath": "[依路徑範本自動整理]",
"errors": { "errors": {
@@ -1226,17 +1228,30 @@
"days": "{count}天後" "days": "{count}天後"
}, },
"badges": { "badges": {
"current": "目前版本", "current": "已開啟版本",
"currentTooltip": "這是你用來開啟此彈窗的版本",
"inLibrary": "已在庫中", "inLibrary": "已在庫中",
"inLibraryTooltip": "此版本已存在於你的本地庫中",
"downloaded": "已下載",
"downloadedTooltip": "此版本之前下載過,但目前不在你的本地庫中",
"newer": "較新版本", "newer": "較新版本",
"newerTooltip": "此版本比你本地的最新版本更新",
"earlyAccess": "搶先體驗", "earlyAccess": "搶先體驗",
"ignored": "已忽略" "earlyAccessTooltip": "此版本目前需要 Civitai 搶先體驗權限",
"ignored": "已忽略",
"ignoredTooltip": "此版本已關閉更新通知"
}, },
"actions": { "actions": {
"download": "下載", "download": "下載",
"downloadTooltip": "下載此版本",
"downloadEarlyAccessTooltip": "從 Civitai 下載此搶先體驗版本",
"delete": "刪除", "delete": "刪除",
"deleteTooltip": "刪除此本地版本",
"ignore": "忽略", "ignore": "忽略",
"unignore": "取消忽略", "unignore": "取消忽略",
"ignoreTooltip": "忽略此版本的更新通知",
"unignoreTooltip": "恢復此版本的更新通知",
"viewVersionOnCivitai": "在 Civitai 上查看版本",
"earlyAccessTooltip": "需要購買搶先體驗", "earlyAccessTooltip": "需要購買搶先體驗",
"resumeModelUpdates": "恢復追蹤此模型的更新", "resumeModelUpdates": "恢復追蹤此模型的更新",
"ignoreModelUpdates": "忽略此模型的更新", "ignoreModelUpdates": "忽略此模型的更新",

View File

@@ -19,6 +19,7 @@ from ...services.download_coordinator import DownloadCoordinator
from ...services.metadata_sync_service import MetadataSyncService from ...services.metadata_sync_service import MetadataSyncService
from ...services.model_file_service import ModelMoveService from ...services.model_file_service import ModelMoveService
from ...services.preview_asset_service import PreviewAssetService from ...services.preview_asset_service import PreviewAssetService
from ...services.service_registry import ServiceRegistry
from ...services.settings_manager import SettingsManager, get_settings_manager from ...services.settings_manager import SettingsManager, get_settings_manager
from ...services.tag_update_service import TagUpdateService from ...services.tag_update_service import TagUpdateService
from ...services.use_cases import ( from ...services.use_cases import (
@@ -1531,6 +1532,20 @@ class ModelCivitaiHandler:
cache = await self._service.scanner.get_cached_data() cache = await self._service.scanner.get_cached_data()
version_index = cache.version_index version_index = cache.version_index
downloaded_version_ids: set[int] = set()
try:
history_service = await ServiceRegistry.get_downloaded_version_history_service()
downloaded_version_ids = set(
await history_service.get_downloaded_version_ids(
self._service.model_type,
model_id,
)
)
except Exception as exc: # pragma: no cover - defensive logging
self._logger.debug(
"Failed to load download history for CivitAI versions: %s",
exc,
)
for version in versions: for version in versions:
version_id = None version_id = None
@@ -1547,6 +1562,9 @@ class ModelCivitaiHandler:
else None else None
) )
version["existsLocally"] = cache_entry is not None version["existsLocally"] = cache_entry is not None
version["hasBeenDownloaded"] = (
version_id in downloaded_version_ids if version_id is not None else False
)
if cache_entry and isinstance(cache_entry, Mapping): if cache_entry and isinstance(cache_entry, Mapping):
local_path = cache_entry.get("file_path") local_path = cache_entry.get("file_path")
if local_path: if local_path:
@@ -2265,7 +2283,7 @@ class ModelUpdateHandler:
self, self,
record, record,
*, *,
version_context: Optional[Dict[int, Dict[str, Optional[str]]]] = None, version_context: Optional[Dict[int, Dict[str, Any]]] = None,
) -> Dict: ) -> Dict:
context = version_context or {} context = version_context or {}
# Check user setting for hiding early access versions # Check user setting for hiding early access versions
@@ -2294,7 +2312,7 @@ class ModelUpdateHandler:
@staticmethod @staticmethod
def _serialize_version( def _serialize_version(
version, context: Optional[Dict[str, Optional[str]]] version, context: Optional[Dict[str, Any]]
) -> Dict: ) -> Dict:
context = context or {} context = context or {}
preview_override = context.get("preview_override") preview_override = context.get("preview_override")
@@ -2328,6 +2346,7 @@ class ModelUpdateHandler:
"sizeBytes": version.size_bytes, "sizeBytes": version.size_bytes,
"previewUrl": preview_url, "previewUrl": preview_url,
"isInLibrary": version.is_in_library, "isInLibrary": version.is_in_library,
"hasBeenDownloaded": bool(context.get("has_been_downloaded", False)),
"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,
@@ -2337,8 +2356,31 @@ class ModelUpdateHandler:
async def _build_version_context( async def _build_version_context(
self, record self, record
) -> Dict[int, Dict[str, Optional[str]]]: ) -> Dict[int, Dict[str, Any]]:
context: Dict[int, Dict[str, Optional[str]]] = {} context: Dict[int, Dict[str, Any]] = {}
downloaded_version_ids: set[int] = set()
try:
history_service = await ServiceRegistry.get_downloaded_version_history_service()
downloaded_version_ids = set(
await history_service.get_downloaded_version_ids(
record.model_type,
record.model_id,
)
)
except Exception as exc: # pragma: no cover - defensive logging
self._logger.debug(
"Failed to load download history while building version context: %s",
exc,
)
for version in record.versions:
context[version.version_id] = {
"file_path": None,
"file_name": None,
"preview_override": None,
"has_been_downloaded": version.version_id in downloaded_version_ids,
}
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
@@ -2357,16 +2399,21 @@ 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]] = { context_entry = context.setdefault(
"file_path": cache_entry.get("file_path"), version.version_id,
"file_name": cache_entry.get("file_name"), {
"file_path": None,
"file_name": None,
"preview_override": None, "preview_override": None,
} "has_been_downloaded": version.version_id in downloaded_version_ids,
},
)
context_entry["file_path"] = cache_entry.get("file_path")
context_entry["file_name"] = cache_entry.get("file_name")
if isinstance(preview, str) and preview: if isinstance(preview, str) and preview:
context_entry["preview_override"] = config.get_preview_static_url( context_entry["preview_override"] = config.get_preview_static_url(
preview preview
) )
context[version.version_id] = context_entry
return context return context

View File

@@ -163,6 +163,18 @@
cursor: pointer; cursor: pointer;
} }
.model-version-row.is-clickable .version-actions,
.model-version-row.is-clickable .version-badges,
.model-version-row.is-clickable .version-action,
.model-version-row.is-clickable .version-civitai-link {
cursor: default;
}
.model-version-row.is-clickable .version-action,
.model-version-row.is-clickable .version-civitai-link {
cursor: pointer;
}
.model-version-row.is-current { .model-version-row.is-current {
border-color: var(--lora-accent); border-color: var(--lora-accent);
box-shadow: 0 0 0 1px color-mix(in oklch, var(--lora-accent) 65%, transparent), box-shadow: 0 0 0 1px color-mix(in oklch, var(--lora-accent) 65%, transparent),
@@ -217,6 +229,7 @@
gap: 8px; gap: 8px;
font-weight: 600; font-weight: 600;
font-size: 0.95rem; font-size: 0.95rem;
min-width: 0;
} }
.versions-tab-version-name { .versions-tab-version-name {
@@ -226,6 +239,27 @@
max-width: 100%; max-width: 100%;
} }
.version-civitai-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 999px;
color: var(--text-muted);
text-decoration: none;
flex: 0 0 auto;
transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
}
.version-civitai-link:hover,
.version-civitai-link:focus-visible {
color: var(--lora-accent);
background: color-mix(in oklch, var(--lora-accent) 12%, transparent);
transform: translateY(-1px);
outline: none;
}
.version-badges { .version-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -21,6 +21,27 @@
font-size: 0.9em; font-size: 0.9em;
} }
.downloaded-badge {
display: inline-flex;
align-items: center;
background: color-mix(in oklch, var(--badge-update-bg, #4a90e2) 22%, transparent);
color: var(--badge-update-bg, #4a90e2);
border: 1px solid color-mix(in oklch, var(--badge-update-bg, #4a90e2) 50%, transparent);
padding: 4px 8px;
border-radius: var(--border-radius-xs);
font-size: 0.8em;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
transform: translateZ(0);
will-change: transform;
}
.downloaded-badge i {
margin-right: 4px;
font-size: 0.9em;
}
/* Early Access Badge */ /* Early Access Badge */
.early-access-badge { .early-access-badge {
display: inline-flex; display: inline-flex;

View File

@@ -220,8 +220,31 @@ function buildMetaMarkup(version, options = {}) {
.join('<span class="version-meta-separator">•</span>'); .join('<span class="version-meta-separator">•</span>');
} }
function buildBadge(label, tone) { function buildBadge(label, tone, options = {}) {
return `<span class="version-badge version-badge-${tone}">${escapeHtml(label)}</span>`; const attributes = [];
if (options.title) {
attributes.push(`title="${escapeHtml(options.title)}"`);
}
if (options.ariaLabel) {
attributes.push(`aria-label="${escapeHtml(options.ariaLabel)}"`);
}
const suffix = attributes.length ? ` ${attributes.join(' ')}` : '';
return `<span class="version-badge version-badge-${tone}"${suffix}>${escapeHtml(label)}</span>`;
}
function buildActionButton(label, variant, action, options = {}) {
const attributes = [
`class="version-action ${variant}"`,
`data-version-action="${escapeHtml(action)}"`,
];
if (options.title) {
attributes.push(`title="${escapeHtml(options.title)}"`);
attributes.push(`aria-label="${escapeHtml(options.title)}"`);
}
if (options.extraAttributes) {
attributes.push(options.extraAttributes);
}
return `<button ${attributes.join(' ')}>${options.iconMarkup || ''}${escapeHtml(label)}</button>`;
} }
const DISPLAY_FILTER_MODES = Object.freeze({ const DISPLAY_FILTER_MODES = Object.freeze({
@@ -426,23 +449,72 @@ function renderRow(version, options) {
version.versionId > latestLibraryVersionId; version.versionId > latestLibraryVersionId;
const isEarlyAccess = isEarlyAccessActive(version); const isEarlyAccess = isEarlyAccessActive(version);
const badges = []; const badges = [];
const openedBadgeLabel = translate('modals.model.versions.badges.current', {}, 'Opened Version');
const inLibraryBadgeLabel = translate('modals.model.versions.badges.inLibrary', {}, 'In Library');
const downloadedBadgeLabel = translate('modals.model.versions.badges.downloaded', {}, 'Downloaded');
const newerBadgeLabel = translate('modals.model.versions.badges.newer', {}, 'Newer Version');
const earlyAccessBadgeLabel = translate('modals.model.versions.badges.earlyAccess', {}, 'Early Access');
const ignoredBadgeLabel = translate('modals.model.versions.badges.ignored', {}, 'Ignored');
const versionName = version.name || translate('modals.model.versions.labels.unnamed', {}, 'Untitled Version');
if (isCurrent) { if (isCurrent) {
badges.push(buildBadge(translate('modals.model.versions.badges.current', {}, 'Current Version'), 'current')); badges.push(buildBadge(openedBadgeLabel, 'current', {
title: translate(
'modals.model.versions.badges.currentTooltip',
{},
'This is the version you opened this modal from'
),
}));
} }
if (version.isInLibrary) { if (version.isInLibrary) {
badges.push(buildBadge(translate('modals.model.versions.badges.inLibrary', {}, 'In Library'), 'success')); badges.push(buildBadge(inLibraryBadgeLabel, 'success', {
} else if (isNewer && !version.shouldIgnore) { title: translate(
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info')); 'modals.model.versions.badges.inLibraryTooltip',
{},
'This version exists in your local library'
),
}));
}
if (!version.isInLibrary && version.hasBeenDownloaded) {
badges.push(buildBadge(downloadedBadgeLabel, 'info', {
title: translate(
'modals.model.versions.badges.downloadedTooltip',
{},
'This version was downloaded before, but is not currently in your library'
),
}));
}
if (!version.isInLibrary && isNewer && !version.shouldIgnore) {
badges.push(buildBadge(newerBadgeLabel, 'info', {
title: translate(
'modals.model.versions.badges.newerTooltip',
{},
'This version is newer than your latest local version'
),
}));
} }
if (isEarlyAccess) { if (isEarlyAccess) {
badges.push(buildBadge(translate('modals.model.versions.badges.earlyAccess', {}, 'Early Access'), 'early-access')); badges.push(buildBadge(earlyAccessBadgeLabel, 'early-access', {
title: translate(
'modals.model.versions.badges.earlyAccessTooltip',
{},
'This version currently requires Civitai early access'
),
}));
} }
if (version.shouldIgnore) { if (version.shouldIgnore) {
badges.push(buildBadge(translate('modals.model.versions.badges.ignored', {}, 'Ignored'), 'muted')); badges.push(buildBadge(ignoredBadgeLabel, 'muted', {
title: translate(
'modals.model.versions.badges.ignoredTooltip',
{},
'Update notifications are disabled for this version'
),
}));
} }
const downloadLabel = translate('modals.model.versions.actions.download', {}, 'Download'); const downloadLabel = translate('modals.model.versions.actions.download', {}, 'Download');
@@ -459,29 +531,82 @@ function renderRow(version, options) {
if (!version.isInLibrary) { if (!version.isInLibrary) {
// Download button with optional EA bolt icon // Download button with optional EA bolt icon
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : ''; const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
actions.push( actions.push(buildActionButton(
`<button class="version-action version-action-primary" data-version-action="download">${downloadIcon}${escapeHtml(downloadLabel)}</button>` downloadLabel,
); 'version-action-primary',
} else if (version.filePath) { 'download',
actions.push( {
`<button class="version-action version-action-danger" data-version-action="delete">${escapeHtml(deleteLabel)}</button>` title: isEarlyAccess
); ? translate(
'modals.model.versions.actions.downloadEarlyAccessTooltip',
{},
'Download this early access version from Civitai'
)
: translate(
'modals.model.versions.actions.downloadTooltip',
{},
'Download this version'
),
iconMarkup: downloadIcon,
} }
actions.push( ));
`<button class="version-action version-action-ghost" data-version-action="toggle-ignore" data-ignore-state="${ } else if (version.filePath) {
version.shouldIgnore ? 'ignored' : 'active' actions.push(buildActionButton(
}">${escapeHtml(ignoreLabel)}</button>` deleteLabel,
); 'version-action-danger',
'delete',
{
title: translate(
'modals.model.versions.actions.deleteTooltip',
{},
'Delete this local version'
),
}
));
}
actions.push(buildActionButton(
ignoreLabel,
'version-action-ghost',
'toggle-ignore',
{
title: version.shouldIgnore
? translate(
'modals.model.versions.actions.unignoreTooltip',
{},
'Resume update notifications for this version'
)
: translate(
'modals.model.versions.actions.ignoreTooltip',
{},
'Ignore update notifications for this version'
),
extraAttributes: `data-ignore-state="${version.shouldIgnore ? 'ignored' : 'active'}"`,
}
));
const linkTarget = buildCivitaiVersionUrl( const linkTarget = buildCivitaiVersionUrl(
version.modelId || parentModelId, version.modelId || parentModelId,
version.versionId version.versionId
); );
const civitaiTooltip = translate( const civitaiTooltip = translate(
'modals.model.actions.viewOnCivitai', 'modals.model.versions.actions.viewVersionOnCivitai',
{}, {},
'View on Civitai' 'View version on Civitai'
); );
const civitaiLinkMarkup = linkTarget
? `
<a
class="version-civitai-link"
href="${escapeHtml(linkTarget)}"
target="_blank"
rel="noopener noreferrer"
title="${escapeHtml(civitaiTooltip)}"
aria-label="${escapeHtml(civitaiTooltip)}"
>
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i>
</a>
`
: '';
const rowAttributes = [ const rowAttributes = [
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}${isEarlyAccess ? ' is-early-access' : ''}"`, `class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}${isEarlyAccess ? ' is-early-access' : ''}"`,
@@ -489,7 +614,6 @@ function renderRow(version, options) {
]; ];
if (linkTarget) { if (linkTarget) {
rowAttributes.push(`data-civitai-url="${escapeHtml(linkTarget)}"`); rowAttributes.push(`data-civitai-url="${escapeHtml(linkTarget)}"`);
rowAttributes.push(`title="${escapeHtml(civitaiTooltip)}"`);
} }
return ` return `
@@ -497,7 +621,8 @@ function renderRow(version, options) {
${renderMediaMarkup(version)} ${renderMediaMarkup(version)}
<div class="version-details"> <div class="version-details">
<div class="version-title"> <div class="version-title">
<span class="versions-tab-version-name">${escapeHtml(version.name || translate('modals.model.versions.labels.unnamed', {}, 'Untitled Version'))}</span> <span class="versions-tab-version-name">${escapeHtml(versionName)}</span>
${civitaiLinkMarkup}
</div> </div>
<div class="version-badges">${badges.join('')}</div> <div class="version-badges">${badges.join('')}</div>
<div class="version-meta"> <div class="version-meta">
@@ -1230,6 +1355,7 @@ export function initVersionsTab({
if (!row) { if (!row) {
return; return;
} }
if (event.target.closest('button')) { if (event.target.closest('button')) {
return; return;
} }

View File

@@ -250,6 +250,7 @@ export class DownloadManager {
(version.files[0]?.sizeKB / 1024).toFixed(2); (version.files[0]?.sizeKB / 1024).toFixed(2);
const existsLocally = version.existsLocally; const existsLocally = version.existsLocally;
const hasBeenDownloaded = version.hasBeenDownloaded && !existsLocally;
const localPath = version.localPath; const localPath = version.localPath;
const isEarlyAccess = version.availability === 'EarlyAccess'; const isEarlyAccess = version.availability === 'EarlyAccess';
@@ -262,11 +263,22 @@ export class DownloadManager {
`; `;
} }
const localStatus = existsLocally ? let localStatus = '';
`<div class="local-badge"> if (existsLocally) {
localStatus = `<div class="local-badge">
<i class="fas fa-check"></i> ${translate('modals.download.inLibrary')} <i class="fas fa-check"></i> ${translate('modals.download.inLibrary')}
<div class="local-path">${localPath || ''}</div> <div class="local-path">${localPath || ''}</div>
</div>` : ''; </div>`;
} else if (hasBeenDownloaded) {
const downloadedTooltip = translate(
'modals.download.downloadedTooltip',
{},
'Previously downloaded, but it is not currently in your library.'
);
localStatus = `<div class="downloaded-badge" title="${downloadedTooltip.replace(/"/g, '&quot;')}">
<i class="fas fa-history"></i> ${translate('modals.download.downloaded', {}, 'Downloaded')}
</div>`;
}
return ` return `
<div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''} <div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''}

View File

@@ -349,4 +349,59 @@ describe('ModelVersionsTab media rendering', () => {
); );
expect(firstBadges).toContain('Newer Version'); expect(firstBadges).toContain('Newer Version');
}); });
it('shows downloaded badge only for previously downloaded versions that are not in library', async () => {
fetchModelUpdateVersions.mockResolvedValue({
success: true,
record: {
shouldIgnore: false,
inLibraryVersionIds: [8],
versions: [
{
versionId: 9,
name: 'History only',
baseModel: 'SDXL',
previewUrl: '/api/lm/previews/v9.png',
sizeBytes: 1024,
isInLibrary: false,
hasBeenDownloaded: true,
shouldIgnore: false,
},
{
versionId: 8,
name: 'Local copy',
baseModel: 'SDXL',
previewUrl: '/api/lm/previews/v8.png',
sizeBytes: 2048,
isInLibrary: true,
hasBeenDownloaded: true,
shouldIgnore: false,
},
],
},
});
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
const controller = initVersionsTab({
modalId: 'model-versions-modal',
modelType: 'loras',
modelId: 654,
currentVersionId: 8,
});
await controller.load();
const rows = document.querySelectorAll('.model-version-row');
const historyBadges = Array.from(rows[0].querySelectorAll('.version-badge')).map(
badge => badge.textContent?.trim()
);
const localBadges = Array.from(rows[1].querySelectorAll('.version-badge')).map(
badge => badge.textContent?.trim()
);
expect(historyBadges).toContain('Downloaded');
expect(historyBadges).not.toContain('In Library');
expect(localBadges).toContain('In Library');
expect(localBadges).not.toContain('Downloaded');
});
}); });

View File

@@ -0,0 +1,139 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const {
DOWNLOAD_MANAGER_MODULE,
MODAL_MANAGER_MODULE,
UI_HELPERS_MODULE,
STATE_MODULE,
LOADING_MANAGER_MODULE,
API_FACTORY_MODULE,
STORAGE_HELPERS_MODULE,
FOLDER_TREE_MANAGER_MODULE,
I18N_HELPERS_MODULE,
} = vi.hoisted(() => ({
DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname,
MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname,
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
LOADING_MANAGER_MODULE: new URL('../../../static/js/managers/LoadingManager.js', import.meta.url).pathname,
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
FOLDER_TREE_MANAGER_MODULE: new URL('../../../static/js/components/FolderTreeManager.js', import.meta.url).pathname,
I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
}));
vi.mock(MODAL_MANAGER_MODULE, () => ({
modalManager: {
showModal: vi.fn(),
closeModal: vi.fn(),
},
}));
vi.mock(UI_HELPERS_MODULE, () => ({
showToast: vi.fn(),
}));
vi.mock(STATE_MODULE, () => ({
state: {
global: {
settings: {},
},
},
}));
vi.mock(LOADING_MANAGER_MODULE, () => ({
LoadingManager: vi.fn(() => ({
showSimpleLoading: vi.fn(),
hide: vi.fn(),
restoreProgressBar: vi.fn(),
showDownloadProgress: vi.fn(() => vi.fn()),
setStatus: vi.fn(),
})),
}));
vi.mock(API_FACTORY_MODULE, () => ({
getModelApiClient: vi.fn(() => ({
apiConfig: {
config: {
displayName: 'LoRA',
singularName: 'lora',
},
},
})),
resetAndReload: vi.fn(),
}));
vi.mock(STORAGE_HELPERS_MODULE, () => ({
getStorageItem: vi.fn((_key, defaultValue) => defaultValue),
setStorageItem: vi.fn(),
}));
vi.mock(FOLDER_TREE_MANAGER_MODULE, () => ({
FolderTreeManager: vi.fn(() => ({
clearSelection: vi.fn(),
init: vi.fn(),
})),
}));
vi.mock(I18N_HELPERS_MODULE, () => ({
translate: vi.fn((_, __, fallback) => fallback ?? ''),
}));
describe('DownloadManager version history badges', () => {
let DownloadManager;
beforeEach(async () => {
vi.resetModules();
document.body.innerHTML = `
<div id="urlStep"></div>
<div id="versionStep"></div>
<div id="versionList"></div>
<button id="nextFromVersion"></button>
`;
({ DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE));
});
afterEach(() => {
document.body.innerHTML = '';
});
it('shows downloaded badge only for versions missing locally', () => {
const manager = new DownloadManager();
manager.versions = [
{
id: 101,
name: 'History only',
images: [],
files: [{ sizeKB: 2048 }],
createdAt: '2026-01-01T00:00:00Z',
existsLocally: false,
hasBeenDownloaded: true,
},
{
id: 102,
name: 'Still local',
images: [],
files: [{ sizeKB: 2048 }],
createdAt: '2026-01-01T00:00:00Z',
existsLocally: true,
hasBeenDownloaded: true,
localPath: '/models/still-local.safetensors',
},
];
manager.showVersionStep();
const items = document.querySelectorAll('.version-item');
expect(items).toHaveLength(2);
expect(items[0].querySelector('.downloaded-badge')?.textContent).toContain('Downloaded');
expect(items[0].querySelector('.downloaded-badge')?.getAttribute('title')).toContain(
'Previously downloaded, but it is not currently in your library.'
);
expect(items[0].querySelector('.local-badge')).toBeNull();
expect(items[1].querySelector('.local-badge')).not.toBeNull();
expect(items[1].querySelector('.local-path')?.textContent).toContain('/models/still-local.safetensors');
expect(items[1].querySelector('.downloaded-badge')).toBeNull();
});
});

View File

@@ -6,7 +6,8 @@ from types import SimpleNamespace
import pytest import pytest
from py.config import config from py.config import config
from py.routes.handlers.model_handlers import ModelUpdateHandler from py.routes.handlers.model_handlers import ModelCivitaiHandler, ModelUpdateHandler
from py.services.service_registry import ServiceRegistry
from py.utils.metadata_manager import MetadataManager from py.utils.metadata_manager import MetadataManager
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
@@ -91,7 +92,131 @@ async def test_build_version_context_includes_static_urls():
overrides = await handler._build_version_context(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: {"file_path": None, "file_name": None, "preview_override": expected}} assert overrides == {
123: {
"file_path": None,
"file_name": None,
"preview_override": expected,
"has_been_downloaded": False,
}
}
@pytest.mark.asyncio
async def test_build_version_context_includes_download_history(monkeypatch):
cache = SimpleNamespace(version_index={})
service = DummyService(cache)
handler = ModelUpdateHandler(
service=service,
update_service=SimpleNamespace(),
metadata_provider_selector=lambda *_: None,
settings_service=SimpleNamespace(get=lambda *_: False),
logger=logging.getLogger(__name__),
)
class DummyHistoryService:
async def get_downloaded_version_ids(self, model_type, model_id):
assert model_type == "lora"
assert model_id == 42
return [123]
async def fake_history_service_factory():
return DummyHistoryService()
monkeypatch.setattr(
ServiceRegistry,
"get_downloaded_version_history_service",
staticmethod(fake_history_service_factory),
)
record = ModelUpdateRecord(
model_type="lora",
model_id=42,
versions=[
ModelVersionRecord(
version_id=123,
name="Downloaded",
base_model=None,
released_at=None,
size_bytes=None,
preview_url=None,
is_in_library=False,
should_ignore=False,
),
ModelVersionRecord(
version_id=124,
name="Fresh",
base_model=None,
released_at=None,
size_bytes=None,
preview_url=None,
is_in_library=False,
should_ignore=False,
),
],
last_checked_at=None,
should_ignore_model=False,
)
overrides = await handler._build_version_context(record)
assert overrides[123]["has_been_downloaded"] is True
assert overrides[124]["has_been_downloaded"] is False
@pytest.mark.asyncio
async def test_get_civitai_versions_degrades_when_download_history_unavailable(monkeypatch):
cache = SimpleNamespace(version_index={})
service = DummyService(cache)
class DummyProvider:
async def get_model_versions(self, model_id):
assert model_id == "42"
return {
"type": "lora",
"modelVersions": [
{
"id": 7,
"name": "Version 7",
"files": [],
}
],
}
async def fake_history_service_factory():
raise RuntimeError("download history unavailable")
monkeypatch.setattr(
ServiceRegistry,
"get_downloaded_version_history_service",
staticmethod(fake_history_service_factory),
)
async def metadata_provider_factory():
return DummyProvider()
handler = ModelCivitaiHandler(
service=service,
settings_service=SimpleNamespace(get=lambda *_: False),
ws_manager=SimpleNamespace(),
logger=logging.getLogger(__name__),
metadata_provider_factory=metadata_provider_factory,
validate_model_type=lambda *_: True,
expected_model_types=lambda: "LoRA",
find_model_file=lambda *_: None,
metadata_sync=SimpleNamespace(),
metadata_refresh_use_case=SimpleNamespace(),
metadata_progress_callback=lambda *_args, **_kwargs: None,
)
response = await handler.get_civitai_versions(
SimpleNamespace(match_info={"model_id": "42"})
)
payload = json.loads(response.text)
assert response.status == 200
assert payload[0]["id"] == 7
assert payload[0]["existsLocally"] is False
assert payload[0]["hasBeenDownloaded"] is False
@pytest.mark.asyncio @pytest.mark.asyncio