mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(early-access): implement EA filtering and UI improvements
Add Early Access version support with filtering and improved UI: Backend: - Add is_early_access and early_access_ends_at fields to ModelVersionRecord - Implement two-phase EA detection (bulk API + single API enrichment) - Add hide_early_access_updates setting to filter EA updates - Update has_update() and has_updates_bulk() to respect EA filter setting - Add _enrich_early_access_details() for precise EA time fetching - Fix setting propagation through base_model_service and model_update_service Frontend: - Add smart relative time display for EA (in Xh, in Xd, or date) - Replace EA label with clock icon in metadata (fa-clock) - Show Download button with bolt icon for EA versions (fa-bolt) - Change EA badge color to #F59F00 (CivitAI Buzz theme) - Fix toggle UI for hide_early_access_updates setting - Add translation keys for EA time formatting Tests: - Update all tests to pass with new EA functionality - Add test coverage for EA filtering logic Closes #815
This commit is contained in:
@@ -426,6 +426,10 @@
|
||||
"any": "Jede verfügbare Aktualisierung markieren"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
|
||||
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "Unbenannte Version",
|
||||
"noDetails": "Keine zusätzlichen Details"
|
||||
"noDetails": "Keine zusätzlichen Details",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "Aktuelle Version",
|
||||
"inLibrary": "In der Bibliothek",
|
||||
"newer": "Neuere Version",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "Ignoriert"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "Löschen",
|
||||
"ignore": "Ignorieren",
|
||||
"unignore": "Ignorierung aufheben",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "Aktualisierungen für dieses Modell fortsetzen",
|
||||
"ignoreModelUpdates": "Aktualisierungen für dieses Modell ignorieren",
|
||||
"viewLocalVersions": "Alle lokalen Versionen anzeigen",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "Flag any available update"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "Hide Early Access Updates",
|
||||
"help": "When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
|
||||
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "Untitled Version",
|
||||
"noDetails": "No additional details"
|
||||
"noDetails": "No additional details",
|
||||
"earlyAccess": "EA"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "ending soon",
|
||||
"hours": "in {count}h",
|
||||
"days": "in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "Current Version",
|
||||
"inLibrary": "In Library",
|
||||
"newer": "Newer Version",
|
||||
"earlyAccess": "Early Access",
|
||||
"ignored": "Ignored"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "Delete",
|
||||
"ignore": "Ignore",
|
||||
"unignore": "Unignore",
|
||||
"earlyAccessTooltip": "Requires early access purchase",
|
||||
"resumeModelUpdates": "Resume updates for this model",
|
||||
"ignoreModelUpdates": "Ignore updates for this model",
|
||||
"viewLocalVersions": "View all local versions",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "Marcar cualquier actualización disponible"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
|
||||
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "Versión sin nombre",
|
||||
"noDetails": "Sin detalles adicionales"
|
||||
"noDetails": "Sin detalles adicionales",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "Versión actual",
|
||||
"inLibrary": "En la biblioteca",
|
||||
"newer": "Versión más reciente",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "Ignorada"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "Eliminar",
|
||||
"ignore": "Ignorar",
|
||||
"unignore": "Dejar de ignorar",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "Reanudar actualizaciones para este modelo",
|
||||
"ignoreModelUpdates": "Ignorar actualizaciones para este modelo",
|
||||
"viewLocalVersions": "Ver todas las versiones locales",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "Signaler n’importe quelle mise à jour disponible"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
|
||||
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "Version sans nom",
|
||||
"noDetails": "Aucun détail supplémentaire"
|
||||
"noDetails": "Aucun détail supplémentaire",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "Version actuelle",
|
||||
"inLibrary": "Dans la bibliothèque",
|
||||
"newer": "Version plus récente",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "Ignorée"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "Supprimer",
|
||||
"ignore": "Ignorer",
|
||||
"unignore": "Ne plus ignorer",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "Reprendre les mises à jour pour ce modèle",
|
||||
"ignoreModelUpdates": "Ignorer les mises à jour pour ce modèle",
|
||||
"viewLocalVersions": "Voir toutes les versions locales",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "תוויות לכל עדכון זמין"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
|
||||
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "גרסה ללא שם",
|
||||
"noDetails": "אין פרטים נוספים"
|
||||
"noDetails": "אין פרטים נוספים",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "גרסה נוכחית",
|
||||
"inLibrary": "בספרייה",
|
||||
"newer": "גרסה חדשה יותר",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "התעלם"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "מחיקה",
|
||||
"ignore": "התעלם",
|
||||
"unignore": "בטל התעלמות",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "המשך עדכונים עבור מודל זה",
|
||||
"ignoreModelUpdates": "התעלם מעדכונים עבור מודל זה",
|
||||
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "利用可能な更新すべてを表示"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
|
||||
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "名前のないバージョン",
|
||||
"noDetails": "追加情報なし"
|
||||
"noDetails": "追加情報なし",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "現在のバージョン",
|
||||
"inLibrary": "ライブラリにあります",
|
||||
"newer": "新しいバージョン",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "無視中"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "削除",
|
||||
"ignore": "無視",
|
||||
"unignore": "無視を解除",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "このモデルの更新を再開",
|
||||
"ignoreModelUpdates": "このモデルの更新を無視",
|
||||
"viewLocalVersions": "ローカルの全バージョンを表示",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "사용 가능한 모든 업데이트 표시"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
|
||||
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "이름 없는 버전",
|
||||
"noDetails": "추가 정보 없음"
|
||||
"noDetails": "추가 정보 없음",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "현재 버전",
|
||||
"inLibrary": "라이브러리에 있음",
|
||||
"newer": "최신 버전",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "무시됨"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "삭제",
|
||||
"ignore": "무시",
|
||||
"unignore": "무시 해제",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "이 모델 업데이트 재개",
|
||||
"ignoreModelUpdates": "이 모델 업데이트 무시",
|
||||
"viewLocalVersions": "로컬 버전 모두 보기",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "Отмечать любые доступные обновления"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
|
||||
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "Версия без названия",
|
||||
"noDetails": "Дополнительная информация отсутствует"
|
||||
"noDetails": "Дополнительная информация отсутствует",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "Текущая версия",
|
||||
"inLibrary": "В библиотеке",
|
||||
"newer": "Более новая версия",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "Игнорируется"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "Удалить",
|
||||
"ignore": "Игнорировать",
|
||||
"unignore": "Перестать игнорировать",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "Возобновить обновления для этой модели",
|
||||
"ignoreModelUpdates": "Игнорировать обновления для этой модели",
|
||||
"viewLocalVersions": "Показать все локальные версии",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "显示任何可用更新"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
|
||||
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "未命名版本",
|
||||
"noDetails": "暂无更多信息"
|
||||
"noDetails": "暂无更多信息",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "当前版本",
|
||||
"inLibrary": "已在库中",
|
||||
"newer": "较新的版本",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "已忽略"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "删除",
|
||||
"ignore": "忽略",
|
||||
"unignore": "取消忽略",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "继续跟踪该模型的更新",
|
||||
"ignoreModelUpdates": "忽略该模型的更新",
|
||||
"viewLocalVersions": "查看所有本地版本",
|
||||
|
||||
@@ -426,6 +426,10 @@
|
||||
"any": "顯示任何可用更新"
|
||||
}
|
||||
},
|
||||
"hideEarlyAccessUpdates": {
|
||||
"label": "[TODO: Translate] Hide Early Access Updates",
|
||||
"help": "[TODO: Translate] When enabled, models with only early access updates will not show 'Update available' badge"
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
|
||||
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞"
|
||||
@@ -1031,12 +1035,19 @@
|
||||
},
|
||||
"labels": {
|
||||
"unnamed": "未命名版本",
|
||||
"noDetails": "沒有其他資訊"
|
||||
"noDetails": "沒有其他資訊",
|
||||
"earlyAccess": "[TODO: Translate] Early Access"
|
||||
},
|
||||
"eaTime": {
|
||||
"endingSoon": "[TODO: Translate] ending soon",
|
||||
"hours": "[TODO: Translate] in {count}h",
|
||||
"days": "[TODO: Translate] in {count}d"
|
||||
},
|
||||
"badges": {
|
||||
"current": "目前版本",
|
||||
"inLibrary": "已在庫中",
|
||||
"newer": "較新版本",
|
||||
"earlyAccess": "[TODO: Translate] Early Access",
|
||||
"ignored": "已忽略"
|
||||
},
|
||||
"actions": {
|
||||
@@ -1044,6 +1055,7 @@
|
||||
"delete": "刪除",
|
||||
"ignore": "忽略",
|
||||
"unignore": "取消忽略",
|
||||
"earlyAccessTooltip": "[TODO: Translate] Requires early access purchase",
|
||||
"resumeModelUpdates": "恢復追蹤此模型的更新",
|
||||
"ignoreModelUpdates": "忽略此模型的更新",
|
||||
"viewLocalVersions": "檢視所有本地版本",
|
||||
|
||||
@@ -204,6 +204,7 @@ class BaseModelRoutes(ABC):
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=get_metadata_provider,
|
||||
settings_service=self._settings,
|
||||
logger=logger,
|
||||
)
|
||||
return ModelHandlerSet(
|
||||
|
||||
@@ -257,6 +257,7 @@ class SettingsHandler:
|
||||
"auto_organize_exclusions",
|
||||
"metadata_refresh_skip_paths",
|
||||
"filter_presets",
|
||||
"hide_early_access_updates",
|
||||
)
|
||||
|
||||
_PROXY_KEYS = {
|
||||
|
||||
@@ -1533,11 +1533,13 @@ class ModelUpdateHandler:
|
||||
service,
|
||||
update_service,
|
||||
metadata_provider_selector,
|
||||
settings_service,
|
||||
logger: logging.Logger,
|
||||
) -> None:
|
||||
self._service = service
|
||||
self._update_service = update_service
|
||||
self._metadata_provider_selector = metadata_provider_selector
|
||||
self._settings = settings_service
|
||||
self._logger = logger
|
||||
|
||||
async def fetch_missing_civitai_license_data(
|
||||
@@ -1774,6 +1776,9 @@ class ModelUpdateHandler:
|
||||
{"success": False, "error": "Model not tracked"}, status=404
|
||||
)
|
||||
|
||||
# Enrich EA versions with detailed info if needed
|
||||
record = await self._enrich_early_access_details(record)
|
||||
|
||||
overrides = await self._build_version_context(record)
|
||||
return web.json_response(
|
||||
{
|
||||
@@ -1812,6 +1817,78 @@ class ModelUpdateHandler:
|
||||
)
|
||||
return None
|
||||
|
||||
async def _enrich_early_access_details(self, record):
|
||||
"""Fetch detailed EA info for versions missing exact end time.
|
||||
|
||||
Identifies versions with is_early_access=True but no early_access_ends_at,
|
||||
then fetches detailed info from CivitAI to get the exact end time.
|
||||
"""
|
||||
if not record or not record.versions:
|
||||
return record
|
||||
|
||||
# Find versions that need enrichment
|
||||
versions_needing_update = []
|
||||
for version in record.versions:
|
||||
if version.is_early_access and not version.early_access_ends_at:
|
||||
versions_needing_update.append(version)
|
||||
|
||||
if not versions_needing_update:
|
||||
return record
|
||||
|
||||
provider = await self._get_civitai_provider()
|
||||
if not provider:
|
||||
return record
|
||||
|
||||
# Fetch detailed info for each version needing update
|
||||
updated_versions = []
|
||||
for version in versions_needing_update:
|
||||
try:
|
||||
version_info, error = await provider.get_model_version_info(
|
||||
str(version.version_id)
|
||||
)
|
||||
if version_info and not error:
|
||||
ea_ends_at = version_info.get("earlyAccessEndsAt")
|
||||
if ea_ends_at:
|
||||
# Create updated version with EA end time
|
||||
from dataclasses import replace
|
||||
|
||||
updated_version = replace(
|
||||
version, early_access_ends_at=ea_ends_at
|
||||
)
|
||||
updated_versions.append(updated_version)
|
||||
self._logger.debug(
|
||||
"Enriched EA info for version %s: %s",
|
||||
version.version_id,
|
||||
ea_ends_at,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._logger.debug(
|
||||
"Failed to fetch EA details for version %s: %s",
|
||||
version.version_id,
|
||||
exc,
|
||||
)
|
||||
|
||||
if not updated_versions:
|
||||
return record
|
||||
|
||||
# Update record with enriched versions
|
||||
version_map = {v.version_id: v for v in record.versions}
|
||||
for updated in updated_versions:
|
||||
version_map[updated.version_id] = updated
|
||||
|
||||
# Create new record with updated versions
|
||||
from dataclasses import replace
|
||||
|
||||
new_record = replace(
|
||||
record, versions=list(version_map.values()),
|
||||
)
|
||||
|
||||
# Optionally persist to database for caching
|
||||
# Note: We don't persist here to avoid side effects; the data will be
|
||||
# refreshed on next bulk update if still needed
|
||||
|
||||
return new_record
|
||||
|
||||
async def _collect_models_missing_license(
|
||||
self,
|
||||
cache,
|
||||
@@ -1978,6 +2055,15 @@ class ModelUpdateHandler:
|
||||
version_context: Optional[Dict[int, Dict[str, Optional[str]]]] = None,
|
||||
) -> Dict:
|
||||
context = version_context or {}
|
||||
# Check user setting for hiding early access versions
|
||||
hide_early_access = False
|
||||
if self._settings is not None:
|
||||
try:
|
||||
hide_early_access = bool(
|
||||
self._settings.get("hide_early_access_updates", False)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"modelType": record.model_type,
|
||||
"modelId": record.model_id,
|
||||
@@ -1986,7 +2072,7 @@ class ModelUpdateHandler:
|
||||
"inLibraryVersionIds": record.in_library_version_ids,
|
||||
"lastCheckedAt": record.last_checked_at,
|
||||
"shouldIgnore": record.should_ignore_model,
|
||||
"hasUpdate": record.has_update(),
|
||||
"hasUpdate": record.has_update(hide_early_access=hide_early_access),
|
||||
"versions": [
|
||||
self._serialize_version(version, context.get(version.version_id))
|
||||
for version in record.versions
|
||||
@@ -2002,6 +2088,24 @@ class ModelUpdateHandler:
|
||||
preview_url = (
|
||||
preview_override if preview_override is not None else version.preview_url
|
||||
)
|
||||
|
||||
# Determine if version is currently in early access
|
||||
# Two-phase detection: use exact end time if available, otherwise fallback to basic flag
|
||||
is_early_access = False
|
||||
if version.early_access_ends_at:
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
ea_date = datetime.fromisoformat(
|
||||
version.early_access_ends_at.replace("Z", "+00:00")
|
||||
)
|
||||
is_early_access = ea_date > datetime.now(timezone.utc)
|
||||
except (ValueError, AttributeError):
|
||||
# If date parsing fails, treat as active EA (conservative)
|
||||
is_early_access = True
|
||||
elif getattr(version, 'is_early_access', False):
|
||||
# Fallback to basic EA flag from bulk API
|
||||
is_early_access = True
|
||||
|
||||
return {
|
||||
"versionId": version.version_id,
|
||||
"name": version.name,
|
||||
@@ -2011,6 +2115,8 @@ class ModelUpdateHandler:
|
||||
"previewUrl": preview_url,
|
||||
"isInLibrary": version.is_in_library,
|
||||
"shouldIgnore": version.should_ignore,
|
||||
"earlyAccessEndsAt": version.early_access_ends_at,
|
||||
"isEarlyAccess": is_early_access,
|
||||
"filePath": context.get("file_path"),
|
||||
"fileName": context.get("file_name"),
|
||||
}
|
||||
|
||||
@@ -380,6 +380,13 @@ class BaseModelService(ABC):
|
||||
strategy = "same_base"
|
||||
same_base_mode = strategy == "same_base"
|
||||
|
||||
# Check user setting for hiding early access updates
|
||||
hide_early_access = False
|
||||
try:
|
||||
hide_early_access = bool(self.settings.get("hide_early_access_updates", False))
|
||||
except Exception:
|
||||
hide_early_access = False
|
||||
|
||||
records = None
|
||||
resolved: Optional[Dict[int, bool]] = None
|
||||
if same_base_mode:
|
||||
@@ -388,7 +395,7 @@ class BaseModelService(ABC):
|
||||
try:
|
||||
records = await record_method(self.model_type, ordered_ids)
|
||||
resolved = {
|
||||
model_id: record.has_update()
|
||||
model_id: record.has_update(hide_early_access=hide_early_access)
|
||||
for model_id, record in records.items()
|
||||
}
|
||||
except Exception as exc:
|
||||
@@ -406,7 +413,7 @@ class BaseModelService(ABC):
|
||||
bulk_method = getattr(self.update_service, "has_updates_bulk", None)
|
||||
if callable(bulk_method):
|
||||
try:
|
||||
resolved = await bulk_method(self.model_type, ordered_ids)
|
||||
resolved = await bulk_method(self.model_type, ordered_ids, hide_early_access=hide_early_access)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to resolve update status in bulk for %s models (%s): %s",
|
||||
@@ -419,7 +426,7 @@ class BaseModelService(ABC):
|
||||
|
||||
if resolved is None:
|
||||
tasks = [
|
||||
self.update_service.has_update(self.model_type, model_id)
|
||||
self.update_service.has_update(self.model_type, model_id, hide_early_access=hide_early_access)
|
||||
for model_id in ordered_ids
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
@@ -457,6 +464,7 @@ class BaseModelService(ABC):
|
||||
flag = record.has_update_for_base(
|
||||
threshold_version,
|
||||
base_model,
|
||||
hide_early_access=hide_early_access,
|
||||
)
|
||||
else:
|
||||
flag = default_flag
|
||||
|
||||
@@ -7,7 +7,8 @@ import os
|
||||
import sqlite3
|
||||
import time
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Dict, Iterable, List, Mapping, Optional, Sequence
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
||||
|
||||
from .errors import RateLimitError, ResourceNotFoundError
|
||||
from .settings_manager import get_settings_manager
|
||||
@@ -64,7 +65,9 @@ class ModelVersionRecord:
|
||||
preview_url: Optional[str]
|
||||
is_in_library: bool
|
||||
should_ignore: bool
|
||||
early_access_ends_at: Optional[str] = None
|
||||
sort_index: int = 0
|
||||
is_early_access: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -97,8 +100,12 @@ class ModelUpdateRecord:
|
||||
|
||||
return [version.version_id for version in self.versions if version.is_in_library]
|
||||
|
||||
def has_update(self) -> bool:
|
||||
"""Return True when a non-ignored remote version newer than the newest local copy is available."""
|
||||
def has_update(self, hide_early_access: bool = False) -> bool:
|
||||
"""Return True when a non-ignored remote version newer than the newest local copy is available.
|
||||
|
||||
Args:
|
||||
hide_early_access: If True, exclude early access versions from update check.
|
||||
"""
|
||||
|
||||
if self.should_ignore_model:
|
||||
return False
|
||||
@@ -110,22 +117,56 @@ class ModelUpdateRecord:
|
||||
|
||||
if max_in_library is None:
|
||||
return any(
|
||||
not version.is_in_library and not version.should_ignore for version in self.versions
|
||||
not version.is_in_library
|
||||
and not version.should_ignore
|
||||
and not (hide_early_access and ModelUpdateRecord._is_early_access_active(version))
|
||||
for version in self.versions
|
||||
)
|
||||
|
||||
for version in self.versions:
|
||||
if version.is_in_library or version.should_ignore:
|
||||
continue
|
||||
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
||||
continue
|
||||
if version.version_id > max_in_library:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_early_access_active(version: ModelVersionRecord) -> bool:
|
||||
"""Check if a version is currently in early access period.
|
||||
|
||||
Uses two-phase detection:
|
||||
1. If exact EA end time available (from single version API), use it for precise check
|
||||
2. Otherwise fallback to basic EA flag (from bulk API)
|
||||
"""
|
||||
# Phase 2: Precise check with exact end time
|
||||
if version.early_access_ends_at:
|
||||
try:
|
||||
ea_date = datetime.fromisoformat(
|
||||
version.early_access_ends_at.replace("Z", "+00:00")
|
||||
)
|
||||
return ea_date > datetime.now(timezone.utc)
|
||||
except (ValueError, AttributeError):
|
||||
# If date parsing fails, treat as active EA (conservative)
|
||||
return True
|
||||
|
||||
# Phase 1: Basic EA flag from bulk API
|
||||
return version.is_early_access
|
||||
|
||||
def has_update_for_base(
|
||||
self,
|
||||
local_version_id: Optional[int],
|
||||
local_base_model: Optional[str],
|
||||
hide_early_access: bool = False,
|
||||
) -> bool:
|
||||
"""Return True when a newer remote version with the same base model exists."""
|
||||
"""Return True when a newer remote version with the same base model exists.
|
||||
|
||||
Args:
|
||||
local_version_id: The current local version id.
|
||||
local_base_model: The base model to filter by.
|
||||
hide_early_access: If True, exclude early access versions from update check.
|
||||
"""
|
||||
|
||||
if self.should_ignore_model:
|
||||
return False
|
||||
@@ -153,6 +194,8 @@ class ModelUpdateRecord:
|
||||
for version in self.versions:
|
||||
if version.is_in_library or version.should_ignore:
|
||||
continue
|
||||
if hide_early_access and ModelUpdateRecord._is_early_access_active(version):
|
||||
continue
|
||||
version_base = _normalize_base_model(version.base_model)
|
||||
if version_base != normalized_base:
|
||||
continue
|
||||
@@ -268,6 +311,14 @@ class ModelUpdateService:
|
||||
"ALTER TABLE model_update_versions "
|
||||
"ADD COLUMN should_ignore INTEGER NOT NULL DEFAULT 0"
|
||||
),
|
||||
"early_access_ends_at": (
|
||||
"ALTER TABLE model_update_versions "
|
||||
"ADD COLUMN early_access_ends_at TEXT"
|
||||
),
|
||||
"is_early_access": (
|
||||
"ALTER TABLE model_update_versions "
|
||||
"ADD COLUMN is_early_access INTEGER NOT NULL DEFAULT 0"
|
||||
),
|
||||
}
|
||||
|
||||
for column, statement in migrations.items():
|
||||
@@ -367,6 +418,8 @@ class ModelUpdateService:
|
||||
preview_url TEXT,
|
||||
is_in_library INTEGER NOT NULL DEFAULT 0,
|
||||
should_ignore INTEGER NOT NULL DEFAULT 0,
|
||||
early_access_ends_at TEXT,
|
||||
is_early_access INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (model_id, version_id),
|
||||
FOREIGN KEY(model_id) REFERENCES model_update_status(model_id) ON DELETE CASCADE
|
||||
)
|
||||
@@ -384,6 +437,8 @@ class ModelUpdateService:
|
||||
"preview_url",
|
||||
"is_in_library",
|
||||
"should_ignore",
|
||||
"early_access_ends_at",
|
||||
"is_early_access",
|
||||
]
|
||||
defaults = {
|
||||
"sort_index": "0",
|
||||
@@ -394,6 +449,8 @@ class ModelUpdateService:
|
||||
"preview_url": "NULL",
|
||||
"is_in_library": "0",
|
||||
"should_ignore": "0",
|
||||
"early_access_ends_at": "NULL",
|
||||
"is_early_access": "0",
|
||||
}
|
||||
|
||||
select_parts = []
|
||||
@@ -667,6 +724,8 @@ class ModelUpdateService:
|
||||
is_in_library=False,
|
||||
should_ignore=should_ignore,
|
||||
sort_index=len(versions),
|
||||
early_access_ends_at=None,
|
||||
is_early_access=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -686,16 +745,17 @@ class ModelUpdateService:
|
||||
async with self._lock:
|
||||
return self._get_record(model_type, model_id)
|
||||
|
||||
async def has_update(self, model_type: str, model_id: int) -> bool:
|
||||
async def has_update(self, model_type: str, model_id: int, hide_early_access: bool = False) -> bool:
|
||||
"""Determine if a model has updates pending."""
|
||||
|
||||
record = await self.get_record(model_type, model_id)
|
||||
return record.has_update() if record else False
|
||||
return record.has_update(hide_early_access=hide_early_access) if record else False
|
||||
|
||||
async def has_updates_bulk(
|
||||
self,
|
||||
model_type: str,
|
||||
model_ids: Sequence[int],
|
||||
hide_early_access: bool = False,
|
||||
) -> Dict[int, bool]:
|
||||
"""Return update availability for each model id in a single database pass."""
|
||||
|
||||
@@ -707,7 +767,7 @@ class ModelUpdateService:
|
||||
records = self._get_records_bulk(model_type, normalized_ids)
|
||||
|
||||
return {
|
||||
model_id: records.get(model_id).has_update() if records.get(model_id) else False
|
||||
model_id: records.get(model_id).has_update(hide_early_access=hide_early_access) if records.get(model_id) else False
|
||||
for model_id in normalized_ids
|
||||
}
|
||||
|
||||
@@ -987,6 +1047,8 @@ class ModelUpdateService:
|
||||
is_in_library=True,
|
||||
should_ignore=ignore_map.get(missing_id, False),
|
||||
sort_index=len(versions),
|
||||
early_access_ends_at=None,
|
||||
is_early_access=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1029,6 +1091,8 @@ class ModelUpdateService:
|
||||
is_in_library=version_id in local_set,
|
||||
should_ignore=ignore_map.get(version_id, remote_version.should_ignore),
|
||||
sort_index=sort_map.get(version_id, index),
|
||||
early_access_ends_at=remote_version.early_access_ends_at,
|
||||
is_early_access=remote_version.is_early_access,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1055,6 +1119,8 @@ class ModelUpdateService:
|
||||
is_in_library=True,
|
||||
should_ignore=ignore_map.get(version_id, False),
|
||||
sort_index=len(versions),
|
||||
early_access_ends_at=None,
|
||||
is_early_access=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1120,6 +1186,11 @@ class ModelUpdateService:
|
||||
released_at = _normalize_string(entry.get("publishedAt") or entry.get("createdAt"))
|
||||
size_bytes = self._extract_size_bytes(entry.get("files"))
|
||||
preview_url = self._extract_preview_url(entry.get("images"))
|
||||
early_access_ends_at = _normalize_string(entry.get("earlyAccessEndsAt"))
|
||||
|
||||
# Check availability field from bulk API for basic EA detection
|
||||
availability = _normalize_string(entry.get("availability"))
|
||||
is_early_access = availability == "EarlyAccess"
|
||||
|
||||
return ModelVersionRecord(
|
||||
version_id=version_id,
|
||||
@@ -1130,7 +1201,9 @@ class ModelUpdateService:
|
||||
preview_url=preview_url,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
early_access_ends_at=early_access_ends_at,
|
||||
sort_index=index,
|
||||
is_early_access=is_early_access,
|
||||
)
|
||||
|
||||
def _extract_size_bytes(self, files) -> Optional[int]:
|
||||
@@ -1231,7 +1304,8 @@ class ModelUpdateService:
|
||||
version_rows = conn.execute(
|
||||
f"""
|
||||
SELECT model_id, version_id, sort_index, name, base_model, released_at,
|
||||
size_bytes, preview_url, is_in_library, should_ignore
|
||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||
is_early_access
|
||||
FROM model_update_versions
|
||||
WHERE model_id IN ({placeholders})
|
||||
ORDER BY model_id ASC, sort_index ASC, version_id ASC
|
||||
@@ -1252,7 +1326,9 @@ class ModelUpdateService:
|
||||
preview_url=row["preview_url"],
|
||||
is_in_library=bool(row["is_in_library"]),
|
||||
should_ignore=bool(row["should_ignore"]),
|
||||
early_access_ends_at=row["early_access_ends_at"],
|
||||
sort_index=_normalize_int(row["sort_index"]) or 0,
|
||||
is_early_access=bool(row["is_early_access"]),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1308,8 +1384,9 @@ class ModelUpdateService:
|
||||
"""
|
||||
INSERT INTO model_update_versions (
|
||||
version_id, model_id, sort_index, name, base_model, released_at,
|
||||
size_bytes, preview_url, is_in_library, should_ignore
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
size_bytes, preview_url, is_in_library, should_ignore, early_access_ends_at,
|
||||
is_early_access
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
version.version_id,
|
||||
@@ -1322,6 +1399,8 @@ class ModelUpdateService:
|
||||
version.preview_url,
|
||||
1 if version.is_in_library else 0,
|
||||
1 if version.should_ignore else 0,
|
||||
version.early_access_ends_at,
|
||||
1 if version.is_early_access else 0,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -387,3 +387,51 @@
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Early Access styles - Buzz theme color (#F59F00) */
|
||||
.version-badge-early-access {
|
||||
background: color-mix(in oklch, #F59F00 25%, transparent);
|
||||
color: #E67700;
|
||||
border-color: color-mix(in oklch, #F59F00 55%, transparent);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-badge-early-access {
|
||||
background: color-mix(in oklch, #F59F00 20%, transparent);
|
||||
color: #F59F00;
|
||||
border-color: color-mix(in oklch, #F59F00 45%, transparent);
|
||||
}
|
||||
|
||||
.version-meta-ea {
|
||||
color: #E67700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-meta-ea {
|
||||
color: #F59F00;
|
||||
}
|
||||
|
||||
/* Early Access row - gray out effect */
|
||||
.model-version-row.is-early-access {
|
||||
opacity: 0.85;
|
||||
filter: grayscale(40%);
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
.model-version-row.is-early-access:hover {
|
||||
opacity: 0.95;
|
||||
filter: grayscale(25%);
|
||||
}
|
||||
|
||||
/* Early Access download button - Buzz theme color (#F59F00) */
|
||||
.version-action-early-access {
|
||||
background: color-mix(in oklch, #F59F00 15%, transparent);
|
||||
color: #E67700;
|
||||
border-color: color-mix(in oklch, #F59F00 50%, transparent);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-action-early-access {
|
||||
background: color-mix(in oklch, #F59F00 12%, transparent);
|
||||
color: #F59F00;
|
||||
border-color: color-mix(in oklch, #F59F00 40%, transparent);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,70 @@ function formatDateLabel(value) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildMetaMarkup(version) {
|
||||
/**
|
||||
* Format EA end time as smart relative time
|
||||
* - < 1 day: "in Xh" (hours)
|
||||
* - 1-7 days: "in Xd" (days)
|
||||
* - > 7 days: "Jan 15" (short date)
|
||||
*/
|
||||
function formatEarlyAccessTime(endsAt) {
|
||||
if (!endsAt) {
|
||||
return null;
|
||||
}
|
||||
const endDate = new Date(endsAt);
|
||||
if (Number.isNaN(endDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = endDate.getTime() - now.getTime();
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
const diffDays = diffHours / 24;
|
||||
|
||||
if (diffHours < 1) {
|
||||
return translate('modals.model.versions.eaTime.endingSoon', {}, 'ending soon');
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
const hours = Math.ceil(diffHours);
|
||||
return translate(
|
||||
'modals.model.versions.eaTime.hours',
|
||||
{ count: hours },
|
||||
`in ${hours}h`
|
||||
);
|
||||
}
|
||||
if (diffDays <= 7) {
|
||||
const days = Math.ceil(diffDays);
|
||||
return translate(
|
||||
'modals.model.versions.eaTime.days',
|
||||
{ count: days },
|
||||
`in ${days}d`
|
||||
);
|
||||
}
|
||||
// More than 7 days: show short date
|
||||
return endDate.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function isEarlyAccessActive(version) {
|
||||
// Two-phase detection:
|
||||
// 1. Use pre-computed isEarlyAccess flag if available (from backend)
|
||||
// 2. Otherwise check exact end time if available
|
||||
if (typeof version.isEarlyAccess === 'boolean') {
|
||||
return version.isEarlyAccess;
|
||||
}
|
||||
if (!version.earlyAccessEndsAt) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new Date(version.earlyAccessEndsAt) > new Date();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildMetaMarkup(version, options = {}) {
|
||||
const segments = [];
|
||||
if (version.baseModel) {
|
||||
segments.push(
|
||||
@@ -138,6 +201,14 @@ function buildMetaMarkup(version) {
|
||||
segments.push(escapeHtml(formatFileSize(version.sizeBytes)));
|
||||
}
|
||||
|
||||
// Add early access info if applicable
|
||||
if (options.showEarlyAccess && isEarlyAccessActive(version)) {
|
||||
const eaTime = formatEarlyAccessTime(version.earlyAccessEndsAt);
|
||||
if (eaTime) {
|
||||
segments.push(`<span class="version-meta-ea"><i class="fas fa-clock"></i> ${escapeHtml(eaTime)}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!segments.length) {
|
||||
return escapeHtml(
|
||||
translate('modals.model.versions.labels.noDetails', {}, 'No additional details')
|
||||
@@ -235,6 +306,7 @@ function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
||||
|
||||
const strategy = state?.global?.settings?.update_flag_strategy;
|
||||
const sameBaseMode = strategy === DISPLAY_FILTER_MODES.SAME_BASE;
|
||||
const hideEarlyAccess = state?.global?.settings?.hide_early_access_updates;
|
||||
|
||||
if (!sameBaseMode) {
|
||||
return Boolean(record?.hasUpdate);
|
||||
@@ -278,6 +350,9 @@ function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
||||
if (version.isInLibrary || version.shouldIgnore) {
|
||||
return false;
|
||||
}
|
||||
if (hideEarlyAccess && isEarlyAccessActive(version)) {
|
||||
return false;
|
||||
}
|
||||
const versionBase = normalizeBaseModelName(version.baseModel);
|
||||
if (versionBase !== normalizedBase) {
|
||||
return false;
|
||||
@@ -349,6 +424,7 @@ function renderRow(version, options) {
|
||||
const isNewer =
|
||||
typeof latestLibraryVersionId === 'number' &&
|
||||
version.versionId > latestLibraryVersionId;
|
||||
const isEarlyAccess = isEarlyAccessActive(version);
|
||||
const badges = [];
|
||||
|
||||
if (isCurrent) {
|
||||
@@ -361,6 +437,10 @@ function renderRow(version, options) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
|
||||
}
|
||||
|
||||
if (isEarlyAccess) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.earlyAccess', {}, 'Early Access'), 'early-access'));
|
||||
}
|
||||
|
||||
if (version.shouldIgnore) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.ignored', {}, 'Ignored'), 'muted'));
|
||||
}
|
||||
@@ -377,8 +457,10 @@ function renderRow(version, options) {
|
||||
|
||||
const actions = [];
|
||||
if (!version.isInLibrary) {
|
||||
// Download button with optional EA bolt icon
|
||||
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
||||
actions.push(
|
||||
`<button class="version-action version-action-primary" data-version-action="download">${escapeHtml(downloadLabel)}</button>`
|
||||
`<button class="version-action version-action-primary" data-version-action="download">${downloadIcon}${escapeHtml(downloadLabel)}</button>`
|
||||
);
|
||||
} else if (version.filePath) {
|
||||
actions.push(
|
||||
@@ -402,7 +484,7 @@ function renderRow(version, options) {
|
||||
);
|
||||
|
||||
const rowAttributes = [
|
||||
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}"`,
|
||||
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}${isEarlyAccess ? ' is-early-access' : ''}"`,
|
||||
`data-version-id="${escapeHtml(version.versionId)}"`,
|
||||
];
|
||||
if (linkTarget) {
|
||||
@@ -419,7 +501,7 @@ function renderRow(version, options) {
|
||||
</div>
|
||||
<div class="version-badges">${badges.join('')}</div>
|
||||
<div class="version-meta">
|
||||
${buildMetaMarkup(version)}
|
||||
${buildMetaMarkup(version, { showEarlyAccess: true })}
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
|
||||
@@ -475,6 +475,12 @@ export class SettingsManager {
|
||||
updateFlagStrategySelect.value = state.global.settings.update_flag_strategy || 'same_base';
|
||||
}
|
||||
|
||||
// Set hide early access updates setting
|
||||
const hideEarlyAccessUpdatesCheckbox = document.getElementById('hideEarlyAccessUpdates');
|
||||
if (hideEarlyAccessUpdatesCheckbox) {
|
||||
hideEarlyAccessUpdatesCheckbox.checked = state.global.settings.hide_early_access_updates || false;
|
||||
}
|
||||
|
||||
// Set optimize example images setting
|
||||
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
||||
if (optimizeExampleImagesCheckbox) {
|
||||
|
||||
@@ -34,6 +34,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
update_flag_strategy: 'same_base',
|
||||
hide_early_access_updates: false,
|
||||
auto_organize_exclusions: [],
|
||||
metadata_refresh_skip_paths: [],
|
||||
});
|
||||
|
||||
@@ -355,6 +355,23 @@
|
||||
{{ t('settings.updateFlagStrategy.help') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="hideEarlyAccessUpdates">{{ t('settings.hideEarlyAccessUpdates.label') }}</label>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="hideEarlyAccessUpdates"
|
||||
onchange="settingsManager.saveToggleSetting('hideEarlyAccessUpdates', 'hide_early_access_updates')">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
{{ t('settings.hideEarlyAccessUpdates.help') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Path Customization Section -->
|
||||
|
||||
@@ -66,6 +66,7 @@ async def test_build_version_context_includes_static_urls():
|
||||
service=service,
|
||||
update_service=SimpleNamespace(),
|
||||
metadata_provider_selector=lambda *_: None,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
@@ -145,6 +146,7 @@ async def test_refresh_model_updates_filters_records_without_updates():
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=metadata_selector,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
@@ -207,6 +209,7 @@ async def test_refresh_model_updates_with_target_ids():
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=metadata_selector,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
@@ -258,6 +261,7 @@ async def test_refresh_model_updates_accepts_snake_case_ids():
|
||||
service=service,
|
||||
update_service=update_service,
|
||||
metadata_provider_selector=metadata_selector,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
@@ -337,6 +341,7 @@ async def test_fetch_missing_license_data_updates_metadata(monkeypatch):
|
||||
service=DummyService(cache),
|
||||
update_service=SimpleNamespace(),
|
||||
metadata_provider_selector=metadata_selector,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
@@ -423,6 +428,7 @@ async def test_fetch_missing_license_data_filters_model_ids(monkeypatch):
|
||||
service=DummyService(cache),
|
||||
update_service=SimpleNamespace(),
|
||||
metadata_provider_selector=metadata_selector,
|
||||
settings_service=SimpleNamespace(get=lambda *_: False),
|
||||
logger=logging.getLogger(__name__),
|
||||
)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class StubUpdateService:
|
||||
self.bulk_calls = []
|
||||
self.bulk_error = bulk_error
|
||||
|
||||
async def has_updates_bulk(self, model_type, model_ids):
|
||||
async def has_updates_bulk(self, model_type, model_ids, hide_early_access: bool = False):
|
||||
self.bulk_calls.append((model_type, list(model_ids)))
|
||||
if self.bulk_error:
|
||||
raise RuntimeError("bulk failure")
|
||||
@@ -91,7 +91,7 @@ class StubUpdateService:
|
||||
results[model_id] = result
|
||||
return results
|
||||
|
||||
async def has_update(self, model_type, model_id):
|
||||
async def has_update(self, model_type, model_id, hide_early_access: bool = False):
|
||||
self.calls.append((model_type, model_id))
|
||||
result = self.decisions.get(model_id, False)
|
||||
if isinstance(result, Exception):
|
||||
|
||||
Reference in New Issue
Block a user