feat(update-modal): display last 5 release notes instead of single

- Modified backend to fetch last 5 releases from GitHub API
- Updated frontend to iterate through and display multiple releases
- Added latest badge and publish date styling
- Added update.latestBadge translation key to all locales
- Maintains backward compatibility for single changelog display
This commit is contained in:
Will Miao
2026-01-23 22:19:43 +08:00
parent 0bb75fdf77
commit 7bba24c19f
15 changed files with 180 additions and 52 deletions

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "Nach Updates wird gesucht...", "checkingUpdates": "Nach Updates wird gesucht...",
"checkingMessage": "Bitte warten Sie, während wir nach der neuesten Version suchen.", "checkingMessage": "Bitte warten Sie, während wir nach der neuesten Version suchen.",
"showNotifications": "Update-Benachrichtigungen anzeigen", "showNotifications": "Update-Benachrichtigungen anzeigen",
"latestBadge": "Neueste",
"updateProgress": { "updateProgress": {
"preparing": "Update wird vorbereitet...", "preparing": "Update wird vorbereitet...",
"installing": "Update wird installiert...", "installing": "Update wird installiert...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "Checking for updates...", "checkingUpdates": "Checking for updates...",
"checkingMessage": "Please wait while we check for the latest version.", "checkingMessage": "Please wait while we check for the latest version.",
"showNotifications": "Show update notifications", "showNotifications": "Show update notifications",
"latestBadge": "Latest",
"updateProgress": { "updateProgress": {
"preparing": "Preparing update...", "preparing": "Preparing update...",
"installing": "Installing update...", "installing": "Installing update...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "Comprobando actualizaciones...", "checkingUpdates": "Comprobando actualizaciones...",
"checkingMessage": "Por favor espera mientras comprobamos la última versión.", "checkingMessage": "Por favor espera mientras comprobamos la última versión.",
"showNotifications": "Mostrar notificaciones de actualización", "showNotifications": "Mostrar notificaciones de actualización",
"latestBadge": "Último",
"updateProgress": { "updateProgress": {
"preparing": "Preparando actualización...", "preparing": "Preparando actualización...",
"installing": "Instalando actualización...", "installing": "Instalando actualización...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "Vérification des mises à jour...", "checkingUpdates": "Vérification des mises à jour...",
"checkingMessage": "Veuillez patienter pendant la vérification de la dernière version.", "checkingMessage": "Veuillez patienter pendant la vérification de la dernière version.",
"showNotifications": "Afficher les notifications de mise à jour", "showNotifications": "Afficher les notifications de mise à jour",
"latestBadge": "Dernier",
"updateProgress": { "updateProgress": {
"preparing": "Préparation de la mise à jour...", "preparing": "Préparation de la mise à jour...",
"installing": "Installation de la mise à jour...", "installing": "Installation de la mise à jour...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "בודק עדכונים...", "checkingUpdates": "בודק עדכונים...",
"checkingMessage": "אנא המתן בזמן שאנו בודקים את הגרסה האחרונה.", "checkingMessage": "אנא המתן בזמן שאנו בודקים את הגרסה האחרונה.",
"showNotifications": "הצג התראות עדכון", "showNotifications": "הצג התראות עדכון",
"latestBadge": "עדכן",
"updateProgress": { "updateProgress": {
"preparing": "מכין עדכון...", "preparing": "מכין עדכון...",
"installing": "מתקין עדכון...", "installing": "מתקין עדכון...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "更新を確認中...", "checkingUpdates": "更新を確認中...",
"checkingMessage": "最新バージョンを確認しています。お待ちください。", "checkingMessage": "最新バージョンを確認しています。お待ちください。",
"showNotifications": "更新通知を表示", "showNotifications": "更新通知を表示",
"latestBadge": "最新",
"updateProgress": { "updateProgress": {
"preparing": "更新を準備中...", "preparing": "更新を準備中...",
"installing": "更新をインストール中...", "installing": "更新をインストール中...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "업데이트 확인 중...", "checkingUpdates": "업데이트 확인 중...",
"checkingMessage": "최신 버전을 확인하는 동안 잠시 기다려주세요.", "checkingMessage": "최신 버전을 확인하는 동안 잠시 기다려주세요.",
"showNotifications": "업데이트 알림 표시", "showNotifications": "업데이트 알림 표시",
"latestBadge": "최신",
"updateProgress": { "updateProgress": {
"preparing": "업데이트 준비 중...", "preparing": "업데이트 준비 중...",
"installing": "업데이트 설치 중...", "installing": "업데이트 설치 중...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "Проверка обновлений...", "checkingUpdates": "Проверка обновлений...",
"checkingMessage": "Пожалуйста, подождите, пока мы проверяем последнюю версию.", "checkingMessage": "Пожалуйста, подождите, пока мы проверяем последнюю версию.",
"showNotifications": "Показывать уведомления об обновлениях", "showNotifications": "Показывать уведомления об обновлениях",
"latestBadge": "Последний",
"updateProgress": { "updateProgress": {
"preparing": "Подготовка обновления...", "preparing": "Подготовка обновления...",
"installing": "Установка обновления...", "installing": "Установка обновления...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "正在检查更新...", "checkingUpdates": "正在检查更新...",
"checkingMessage": "请稍候,正在检查最新版本。", "checkingMessage": "请稍候,正在检查最新版本。",
"showNotifications": "显示更新通知", "showNotifications": "显示更新通知",
"latestBadge": "最新",
"updateProgress": { "updateProgress": {
"preparing": "正在准备更新...", "preparing": "正在准备更新...",
"installing": "正在安装更新...", "installing": "正在安装更新...",

View File

@@ -1214,6 +1214,7 @@
"checkingUpdates": "正在檢查更新...", "checkingUpdates": "正在檢查更新...",
"checkingMessage": "請稍候,正在檢查最新版本。", "checkingMessage": "請稍候,正在檢查最新版本。",
"showNotifications": "顯示更新通知", "showNotifications": "顯示更新通知",
"latestBadge": "最新",
"updateProgress": { "updateProgress": {
"preparing": "正在準備更新...", "preparing": "正在準備更新...",
"installing": "正在安裝更新...", "installing": "正在安裝更新...",

View File

@@ -45,8 +45,9 @@ class UpdateRoutes:
# Fetch remote version from GitHub # Fetch remote version from GitHub
if nightly: if nightly:
remote_version, changelog = await UpdateRoutes._get_nightly_version() remote_version, changelog = await UpdateRoutes._get_nightly_version()
releases = None
else: else:
remote_version, changelog = await UpdateRoutes._get_remote_version() remote_version, changelog, releases = await UpdateRoutes._get_remote_version()
# Compare versions # Compare versions
if nightly: if nightly:
@@ -59,7 +60,7 @@ class UpdateRoutes:
remote_version.replace('v', '') remote_version.replace('v', '')
) )
return web.json_response({ response_data = {
'success': True, 'success': True,
'current_version': local_version, 'current_version': local_version,
'latest_version': remote_version, 'latest_version': remote_version,
@@ -67,7 +68,13 @@ class UpdateRoutes:
'changelog': changelog, 'changelog': changelog,
'git_info': git_info, 'git_info': git_info,
'nightly': nightly 'nightly': nightly
}) }
# Include releases list for stable mode
if releases is not None:
response_data['releases'] = releases
return web.json_response(response_data)
except NETWORK_EXCEPTIONS as e: except NETWORK_EXCEPTIONS as e:
logger.warning("Network unavailable during update check: %s", e) logger.warning("Network unavailable during update check: %s", e)
@@ -443,42 +450,58 @@ class UpdateRoutes:
return git_info return git_info
@staticmethod @staticmethod
async def _get_remote_version() -> tuple[str, List[str]]: async def _get_remote_version() -> tuple[str, List[str], List[Dict]]:
""" """
Fetch remote version from GitHub Fetch remote version from GitHub
Returns: Returns:
tuple: (version string, changelog list) tuple: (version string, changelog list, releases list)
""" """
repo_owner = "willmiao" repo_owner = "willmiao"
repo_name = "ComfyUI-Lora-Manager" repo_name = "ComfyUI-Lora-Manager"
# Use GitHub API to fetch the latest release # Use GitHub API to fetch the last 5 releases
github_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" github_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases?per_page=5"
try: try:
downloader = await get_downloader() downloader = await get_downloader()
success, data = await downloader.make_request('GET', github_url, custom_headers={'Accept': 'application/vnd.github+json'}) success, data = await downloader.make_request('GET', github_url, custom_headers={'Accept': 'application/vnd.github+json'})
if not success: if not success:
logger.warning(f"Failed to fetch GitHub release: {data}") logger.warning(f"Failed to fetch GitHub releases: {data}")
return "v0.0.0", [] return "v0.0.0", [], []
version = data.get('tag_name', '') # Parse releases
releases = []
for i, release in enumerate(data):
version = release.get('tag_name', '')
if not version.startswith('v'): if not version.startswith('v'):
version = f"v{version}" version = f"v{version}"
# Extract changelog from release notes # Extract changelog from release notes
body = data.get('body', '') body = release.get('body', '')
changelog = UpdateRoutes._parse_changelog(body) changelog = UpdateRoutes._parse_changelog(body)
return version, changelog releases.append({
'version': version,
'changelog': changelog,
'published_at': release.get('published_at', ''),
'is_latest': i == 0
})
# Get latest version and its changelog
if releases:
latest_version = releases[0]['version']
latest_changelog = releases[0]['changelog']
return latest_version, latest_changelog, releases
return "v0.0.0", [], []
except NETWORK_EXCEPTIONS as e: except NETWORK_EXCEPTIONS as e:
logger.warning("Unable to reach GitHub for release info: %s", e) logger.warning("Unable to reach GitHub for release info: %s", e)
return "v0.0.0", [] return "v0.0.0", [], []
except Exception as e: except Exception as e:
logger.error(f"Error fetching remote version: {e}", exc_info=True) logger.error(f"Error fetching remote version: {e}", exc_info=True)
return "v0.0.0", [] return "v0.0.0", [], []
@staticmethod @staticmethod
def _parse_changelog(release_notes: str) -> List[str]: def _parse_changelog(release_notes: str) -> List[str]:

View File

@@ -122,3 +122,48 @@
.changelog-item a:hover { .changelog-item a:hover {
text-decoration: underline; text-decoration: underline;
} }
/* Multiple releases styling */
.changelog-content > .changelog-item + .changelog-item {
margin-top: var(--space-4);
padding-top: var(--space-4);
border-top: 1px solid var(--lora-border);
}
.changelog-item h4 {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
font-size: 1em;
}
.changelog-item .latest-badge {
background-color: var(--lora-accent);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: 500;
text-transform: uppercase;
}
.changelog-item .publish-date {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.6;
font-weight: normal;
}
.changelog-item.latest {
background-color: rgba(66, 153, 225, 0.05);
border-radius: var(--border-radius-sm);
padding: var(--space-3);
border: 1px solid rgba(66, 153, 225, 0.2);
}
[data-theme="dark"] .changelog-item.latest {
background-color: rgba(66, 153, 225, 0.1);
border-color: rgba(66, 153, 225, 0.3);
}

View File

@@ -1,8 +1,4 @@
/* Update Modal Styles */ /* Update Modal Styles */
.update-modal {
max-width: 600px;
}
.update-header { .update-header {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -509,12 +509,66 @@ export class UpdateService {
} }
// Update changelog content if available // Update changelog content if available
if (this.updateInfo && this.updateInfo.changelog) { if (this.updateInfo && (this.updateInfo.changelog || this.updateInfo.releases)) {
const changelogContent = modal.querySelector('.changelog-content'); const changelogContent = modal.querySelector('.changelog-content');
if (changelogContent) { if (changelogContent) {
changelogContent.innerHTML = ''; // Clear existing content changelogContent.innerHTML = ''; // Clear existing content
// Create changelog item // Check if we have multiple releases
const releases = this.updateInfo.releases;
if (releases && Array.isArray(releases) && releases.length > 0) {
// Display multiple releases (up to 5)
releases.forEach(release => {
const changelogItem = document.createElement('div');
changelogItem.className = 'changelog-item';
if (release.is_latest) {
changelogItem.classList.add('latest');
}
const versionHeader = document.createElement('h4');
if (release.is_latest) {
const badge = document.createElement('span');
badge.className = 'latest-badge';
badge.textContent = translate('update.latestBadge', {}, 'Latest');
versionHeader.appendChild(badge);
versionHeader.appendChild(document.createTextNode(' '));
}
const versionSpan = document.createElement('span');
versionSpan.className = 'version';
versionSpan.textContent = `${translate('common.status.version', {}, 'Version')} ${release.version}`;
versionHeader.appendChild(versionSpan);
if (release.published_at) {
const dateSpan = document.createElement('span');
dateSpan.className = 'publish-date';
dateSpan.textContent = this.formatRelativeTime(new Date(release.published_at).getTime());
versionHeader.appendChild(dateSpan);
}
changelogItem.appendChild(versionHeader);
// Create changelog list
const changelogList = document.createElement('ul');
if (release.changelog && release.changelog.length > 0) {
release.changelog.forEach(item => {
const listItem = document.createElement('li');
listItem.innerHTML = this.parseMarkdown(item);
changelogList.appendChild(listItem);
});
} else {
const listItem = document.createElement('li');
listItem.textContent = translate('update.noChangelogAvailable', {}, 'No detailed changelog available.');
changelogList.appendChild(listItem);
}
changelogItem.appendChild(changelogList);
changelogContent.appendChild(changelogItem);
});
} else {
// Fallback: display single changelog (old behavior)
const changelogItem = document.createElement('div'); const changelogItem = document.createElement('div');
changelogItem.className = 'changelog-item'; changelogItem.className = 'changelog-item';
@@ -522,18 +576,15 @@ export class UpdateService {
versionHeader.textContent = `${translate('common.status.version', {}, 'Version')} ${this.latestVersion}`; versionHeader.textContent = `${translate('common.status.version', {}, 'Version')} ${this.latestVersion}`;
changelogItem.appendChild(versionHeader); changelogItem.appendChild(versionHeader);
// Create changelog list
const changelogList = document.createElement('ul'); const changelogList = document.createElement('ul');
if (this.updateInfo.changelog && this.updateInfo.changelog.length > 0) { if (this.updateInfo.changelog && this.updateInfo.changelog.length > 0) {
this.updateInfo.changelog.forEach(item => { this.updateInfo.changelog.forEach(item => {
const listItem = document.createElement('li'); const listItem = document.createElement('li');
// Parse markdown in changelog items
listItem.innerHTML = this.parseMarkdown(item); listItem.innerHTML = this.parseMarkdown(item);
changelogList.appendChild(listItem); changelogList.appendChild(listItem);
}); });
} else { } else {
// If no changelog items available
const listItem = document.createElement('li'); const listItem = document.createElement('li');
listItem.textContent = translate('update.noChangelogAvailable', {}, 'No detailed changelog available. Check GitHub for more information.'); listItem.textContent = translate('update.noChangelogAvailable', {}, 'No detailed changelog available. Check GitHub for more information.');
changelogList.appendChild(listItem); changelogList.appendChild(listItem);
@@ -543,6 +594,7 @@ export class UpdateService {
changelogContent.appendChild(changelogItem); changelogContent.appendChild(changelogItem);
} }
} }
}
// Update GitHub link to point to the specific release if available // Update GitHub link to point to the specific release if available
const githubLink = modal.querySelector('.update-link'); const githubLink = modal.querySelector('.update-link');

View File

@@ -24,11 +24,12 @@ async def test_get_remote_version_offline_logs_without_traceback(monkeypatch, ca
caplog.set_level(logging.WARNING) caplog.set_level(logging.WARNING)
monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(OfflineDownloader())) monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(OfflineDownloader()))
version, changelog = await update_routes.UpdateRoutes._get_remote_version() version, changelog, releases = await update_routes.UpdateRoutes._get_remote_version()
assert version == "v0.0.0" assert version == "v0.0.0"
assert changelog == [] assert changelog == []
assert "Failed to fetch GitHub release" in caplog.text assert releases == []
assert "Failed to fetch GitHub releases" in caplog.text
assert "Cannot connect to host" in caplog.text assert "Cannot connect to host" in caplog.text
assert "Traceback" not in caplog.text assert "Traceback" not in caplog.text
@@ -38,10 +39,11 @@ async def test_get_remote_version_network_error_logs_warning(monkeypatch, caplog
caplog.set_level(logging.WARNING) caplog.set_level(logging.WARNING)
monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader())) monkeypatch.setattr(update_routes, "get_downloader", lambda: _stub_downloader(RaisingDownloader()))
version, changelog = await update_routes.UpdateRoutes._get_remote_version() version, changelog, releases = await update_routes.UpdateRoutes._get_remote_version()
assert version == "v0.0.0" assert version == "v0.0.0"
assert changelog == [] assert changelog == []
assert releases == []
assert "Unable to reach GitHub for release info" in caplog.text assert "Unable to reach GitHub for release info" in caplog.text
assert "Traceback" not in caplog.text assert "Traceback" not in caplog.text