mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
feat(download-history): add downloaded status UX
This commit is contained in:
@@ -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": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "Aktuelle Version",
|
"current": "Aktuelle Version",
|
||||||
"inLibrary": "In der Bibliothek",
|
"inLibrary": "In der Bibliothek",
|
||||||
|
"downloaded": "Heruntergeladen",
|
||||||
"newer": "Neuere Version",
|
"newer": "Neuere Version",
|
||||||
"earlyAccess": "Früher Zugriff",
|
"earlyAccess": "Früher Zugriff",
|
||||||
"ignored": "Ignoriert"
|
"ignored": "Ignoriert"
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "Current Version",
|
"current": "Current Version",
|
||||||
"inLibrary": "In Library",
|
"inLibrary": "In Library",
|
||||||
|
"downloaded": "Downloaded",
|
||||||
"newer": "Newer Version",
|
"newer": "Newer Version",
|
||||||
"earlyAccess": "Early Access",
|
"earlyAccess": "Early Access",
|
||||||
"ignored": "Ignored"
|
"ignored": "Ignored"
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "Versión actual",
|
"current": "Versión actual",
|
||||||
"inLibrary": "En la biblioteca",
|
"inLibrary": "En la biblioteca",
|
||||||
|
"downloaded": "Descargado",
|
||||||
"newer": "Versión más reciente",
|
"newer": "Versión más reciente",
|
||||||
"earlyAccess": "Acceso temprano",
|
"earlyAccess": "Acceso temprano",
|
||||||
"ignored": "Ignorada"
|
"ignored": "Ignorada"
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "Version actuelle",
|
"current": "Version actuelle",
|
||||||
"inLibrary": "Dans la bibliothèque",
|
"inLibrary": "Dans la bibliothèque",
|
||||||
|
"downloaded": "Téléchargé",
|
||||||
"newer": "Version plus récente",
|
"newer": "Version plus récente",
|
||||||
"earlyAccess": "Accès anticipé",
|
"earlyAccess": "Accès anticipé",
|
||||||
"ignored": "Ignorée"
|
"ignored": "Ignorée"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "גישה מוקדמת",
|
"earlyAccess": "גישה מוקדמת",
|
||||||
"earlyAccessTooltip": "נדרשת גישה מוקדמת",
|
"earlyAccessTooltip": "נדרשת גישה מוקדמת",
|
||||||
"inLibrary": "בספרייה",
|
"inLibrary": "בספרייה",
|
||||||
|
"downloaded": "הורד",
|
||||||
|
"downloadedTooltip": "הורד בעבר, אך הוא אינו נמצא כרגע בספרייה שלך.",
|
||||||
"alreadyInLibrary": "כבר בספרייה",
|
"alreadyInLibrary": "כבר בספרייה",
|
||||||
"autoOrganizedPath": "[מאורגן אוטומטית לפי תבנית נתיב]",
|
"autoOrganizedPath": "[מאורגן אוטומטית לפי תבנית נתיב]",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "גרסה נוכחית",
|
"current": "גרסה נוכחית",
|
||||||
"inLibrary": "בספרייה",
|
"inLibrary": "בספרייה",
|
||||||
|
"downloaded": "הורד",
|
||||||
"newer": "גרסה חדשה יותר",
|
"newer": "גרסה חדשה יותר",
|
||||||
"earlyAccess": "גישה מוקדמת",
|
"earlyAccess": "גישה מוקדמת",
|
||||||
"ignored": "התעלם"
|
"ignored": "התעלם"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "アーリーアクセス",
|
"earlyAccess": "アーリーアクセス",
|
||||||
"earlyAccessTooltip": "アーリーアクセスが必要",
|
"earlyAccessTooltip": "アーリーアクセスが必要",
|
||||||
"inLibrary": "ライブラリ内",
|
"inLibrary": "ライブラリ内",
|
||||||
|
"downloaded": "ダウンロード済み",
|
||||||
|
"downloadedTooltip": "以前にダウンロード済みですが、現在はライブラリにありません。",
|
||||||
"alreadyInLibrary": "既にライブラリ内",
|
"alreadyInLibrary": "既にライブラリ内",
|
||||||
"autoOrganizedPath": "[パステンプレートによる自動整理]",
|
"autoOrganizedPath": "[パステンプレートによる自動整理]",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "現在のバージョン",
|
"current": "現在のバージョン",
|
||||||
"inLibrary": "ライブラリにあります",
|
"inLibrary": "ライブラリにあります",
|
||||||
|
"downloaded": "ダウンロード済み",
|
||||||
"newer": "新しいバージョン",
|
"newer": "新しいバージョン",
|
||||||
"earlyAccess": "早期アクセス",
|
"earlyAccess": "早期アクセス",
|
||||||
"ignored": "無視中"
|
"ignored": "無視中"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "얼리 액세스",
|
"earlyAccess": "얼리 액세스",
|
||||||
"earlyAccessTooltip": "얼리 액세스 필요",
|
"earlyAccessTooltip": "얼리 액세스 필요",
|
||||||
"inLibrary": "라이브러리에 있음",
|
"inLibrary": "라이브러리에 있음",
|
||||||
|
"downloaded": "다운로드됨",
|
||||||
|
"downloadedTooltip": "이전에 다운로드했지만 현재 라이브러리에 없습니다.",
|
||||||
"alreadyInLibrary": "이미 라이브러리에 있음",
|
"alreadyInLibrary": "이미 라이브러리에 있음",
|
||||||
"autoOrganizedPath": "[경로 템플릿으로 자동 정리됨]",
|
"autoOrganizedPath": "[경로 템플릿으로 자동 정리됨]",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "현재 버전",
|
"current": "현재 버전",
|
||||||
"inLibrary": "라이브러리에 있음",
|
"inLibrary": "라이브러리에 있음",
|
||||||
|
"downloaded": "다운로드됨",
|
||||||
"newer": "최신 버전",
|
"newer": "최신 버전",
|
||||||
"earlyAccess": "얼리 액세스",
|
"earlyAccess": "얼리 액세스",
|
||||||
"ignored": "무시됨"
|
"ignored": "무시됨"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "Ранний доступ",
|
"earlyAccess": "Ранний доступ",
|
||||||
"earlyAccessTooltip": "Требуется ранний доступ",
|
"earlyAccessTooltip": "Требуется ранний доступ",
|
||||||
"inLibrary": "В библиотеке",
|
"inLibrary": "В библиотеке",
|
||||||
|
"downloaded": "Загружено",
|
||||||
|
"downloadedTooltip": "Ранее загружено, но сейчас этого нет в вашей библиотеке.",
|
||||||
"alreadyInLibrary": "Уже в библиотеке",
|
"alreadyInLibrary": "Уже в библиотеке",
|
||||||
"autoOrganizedPath": "[Автоматически организовано по шаблону пути]",
|
"autoOrganizedPath": "[Автоматически организовано по шаблону пути]",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "Текущая версия",
|
"current": "Текущая версия",
|
||||||
"inLibrary": "В библиотеке",
|
"inLibrary": "В библиотеке",
|
||||||
|
"downloaded": "Загружено",
|
||||||
"newer": "Более новая версия",
|
"newer": "Более новая версия",
|
||||||
"earlyAccess": "Ранний доступ",
|
"earlyAccess": "Ранний доступ",
|
||||||
"ignored": "Игнорируется"
|
"ignored": "Игнорируется"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "早期访问",
|
"earlyAccess": "早期访问",
|
||||||
"earlyAccessTooltip": "需要早期访问权限",
|
"earlyAccessTooltip": "需要早期访问权限",
|
||||||
"inLibrary": "已在库中",
|
"inLibrary": "已在库中",
|
||||||
|
"downloaded": "已下载",
|
||||||
|
"downloadedTooltip": "之前已下载,但当前不在你的库中。",
|
||||||
"alreadyInLibrary": "已存在于库中",
|
"alreadyInLibrary": "已存在于库中",
|
||||||
"autoOrganizedPath": "【已按路径模板自动整理】",
|
"autoOrganizedPath": "【已按路径模板自动整理】",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "当前版本",
|
"current": "当前版本",
|
||||||
"inLibrary": "已在库中",
|
"inLibrary": "已在库中",
|
||||||
|
"downloaded": "已下载",
|
||||||
"newer": "较新的版本",
|
"newer": "较新的版本",
|
||||||
"earlyAccess": "抢先体验",
|
"earlyAccess": "抢先体验",
|
||||||
"ignored": "已忽略"
|
"ignored": "已忽略"
|
||||||
|
|||||||
@@ -957,6 +957,8 @@
|
|||||||
"earlyAccess": "早期存取",
|
"earlyAccess": "早期存取",
|
||||||
"earlyAccessTooltip": "需要早期存取",
|
"earlyAccessTooltip": "需要早期存取",
|
||||||
"inLibrary": "已在庫存",
|
"inLibrary": "已在庫存",
|
||||||
|
"downloaded": "已下載",
|
||||||
|
"downloadedTooltip": "先前已下載,但目前不在你的庫中。",
|
||||||
"alreadyInLibrary": "已在庫存",
|
"alreadyInLibrary": "已在庫存",
|
||||||
"autoOrganizedPath": "[依路徑範本自動整理]",
|
"autoOrganizedPath": "[依路徑範本自動整理]",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -1228,6 +1230,7 @@
|
|||||||
"badges": {
|
"badges": {
|
||||||
"current": "目前版本",
|
"current": "目前版本",
|
||||||
"inLibrary": "已在庫中",
|
"inLibrary": "已在庫中",
|
||||||
|
"downloaded": "已下載",
|
||||||
"newer": "較新版本",
|
"newer": "較新版本",
|
||||||
"earlyAccess": "搶先體驗",
|
"earlyAccess": "搶先體驗",
|
||||||
"ignored": "已忽略"
|
"ignored": "已忽略"
|
||||||
|
|||||||
@@ -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,13 @@ 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
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for version in versions:
|
for version in versions:
|
||||||
version_id = None
|
version_id = None
|
||||||
@@ -1547,6 +1555,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 +2276,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 +2305,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 +2339,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 +2349,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 +2392,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"),
|
{
|
||||||
"preview_override": None,
|
"file_path": None,
|
||||||
}
|
"file_name": 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,56 @@
|
|||||||
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;
|
||||||
|
position: relative;
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloaded-badge i {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloaded-info {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid color-mix(in oklch, var(--badge-update-bg, #4a90e2) 50%, transparent);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: var(--space-1);
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--text-color);
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 320px;
|
||||||
|
transform: translateZ(0);
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloaded-badge:hover .downloaded-info {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* Early Access Badge */
|
/* Early Access Badge */
|
||||||
.early-access-badge {
|
.early-access-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -108,4 +158,4 @@
|
|||||||
color: var(--lora-error);
|
color: var(--lora-error);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -433,7 +433,13 @@ function renderRow(version, options) {
|
|||||||
|
|
||||||
if (version.isInLibrary) {
|
if (version.isInLibrary) {
|
||||||
badges.push(buildBadge(translate('modals.model.versions.badges.inLibrary', {}, 'In Library'), 'success'));
|
badges.push(buildBadge(translate('modals.model.versions.badges.inLibrary', {}, 'In Library'), 'success'));
|
||||||
} else if (isNewer && !version.shouldIgnore) {
|
}
|
||||||
|
|
||||||
|
if (!version.isInLibrary && version.hasBeenDownloaded) {
|
||||||
|
badges.push(buildBadge(translate('modals.model.versions.badges.downloaded', {}, 'Downloaded'), 'info'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!version.isInLibrary && isNewer && !version.shouldIgnore) {
|
||||||
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
|
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
localStatus = `<div class="downloaded-badge">
|
||||||
|
<i class="fas fa-history"></i> ${translate('modals.download.downloaded', {}, 'Downloaded')}
|
||||||
|
<div class="downloaded-info">${translate(
|
||||||
|
'modals.download.downloadedTooltip',
|
||||||
|
{},
|
||||||
|
'Previously downloaded, but it is not currently in your library.'
|
||||||
|
)}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''}
|
<div class="version-item ${this.currentVersion?.id === version.id ? 'selected' : ''}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
139
tests/frontend/managers/downloadManager.history.test.js
Normal file
139
tests/frontend/managers/downloadManager.history.test.js
Normal 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-info')?.textContent).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ 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 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,75 @@ 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
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user