feat: implement version mismatch handling and banner registration in UpdateService

This commit is contained in:
Will Miao
2025-08-12 15:09:45 +08:00
parent 16df548b77
commit b7e3e53697
3 changed files with 184 additions and 5 deletions

View File

@@ -62,6 +62,12 @@ class BannerService {
*/
registerBanner(id, bannerConfig) {
this.banners.set(id, bannerConfig);
// If already initialized, render the banner immediately
if (this.initialized && !this.isBannerDismissed(id) && this.container) {
this.renderBanner(bannerConfig);
this.updateContainerVisibility();
}
}
/**
@@ -88,6 +94,12 @@ class BannerService {
// Remove banner from DOM
const bannerElement = document.querySelector(`[data-banner-id="${bannerId}"]`);
if (bannerElement) {
// Call onRemove callback if provided
const banner = this.banners.get(bannerId);
if (banner && typeof banner.onRemove === 'function') {
banner.onRemove(bannerElement);
}
bannerElement.style.animation = 'banner-slide-up 0.3s ease-in-out forwards';
setTimeout(() => {
bannerElement.remove();
@@ -122,12 +134,16 @@ class BannerService {
bannerElement.className = 'banner-item';
bannerElement.setAttribute('data-banner-id', banner.id);
const actionsHtml = banner.actions ? banner.actions.map(action =>
`<a href="${action.url}" target="_blank" class="banner-action banner-action-${action.type}" rel="noopener noreferrer">
const actionsHtml = banner.actions ? banner.actions.map(action => {
const actionAttribute = action.action ? `data-action="${action.action}"` : '';
const href = action.url ? `href="${action.url}"` : '#';
const target = action.url ? 'target="_blank" rel="noopener noreferrer"' : '';
return `<a ${href ? `href="${href}"` : ''} ${target} class="banner-action banner-action-${action.type}" ${actionAttribute}>
<i class="${action.icon}"></i>
<span>${action.text}</span>
</a>`
).join('') : '';
</a>`;
}).join('') : '';
const dismissButtonHtml = banner.dismissible ?
`<button class="banner-dismiss" onclick="bannerService.dismissBanner('${banner.id}')" title="Dismiss">
@@ -148,6 +164,11 @@ class BannerService {
`;
this.container.appendChild(bannerElement);
// Call onRegister callback if provided
if (typeof banner.onRegister === 'function') {
banner.onRegister(bannerElement);
}
}
/**

View File

@@ -1,5 +1,13 @@
import { modalManager } from './ModalManager.js';
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
import {
getStorageItem,
setStorageItem,
getStoredVersionInfo,
setStoredVersionInfo,
isVersionMatch,
resetDismissedBanner
} from '../utils/storageHelpers.js';
import { bannerService } from './BannerService.js';
export class UpdateService {
constructor() {
@@ -17,6 +25,8 @@ export class UpdateService {
this.lastCheckTime = parseInt(getStorageItem('last_update_check') || '0');
this.isUpdating = false;
this.nightlyMode = getStorageItem('nightly_updates', false);
this.currentVersionInfo = null;
this.versionMismatch = false;
}
initialize() {
@@ -59,6 +69,9 @@ export class UpdateService {
// Immediately update modal content with current values (even if from default)
this.updateModalContent();
// Check version info for mismatch after loading basic info
this.checkVersionInfo();
}
updateNightlyWarning() {
@@ -424,6 +437,110 @@ export class UpdateService {
// Ensure badge visibility is updated after manual check
this.updateBadgeVisibility();
}
async checkVersionInfo() {
try {
// Call API to get current version info
const response = await fetch('/api/version-info');
const data = await response.json();
if (data.success) {
this.currentVersionInfo = data.version;
// Check if version matches stored version
this.versionMismatch = !isVersionMatch(this.currentVersionInfo);
if (this.versionMismatch) {
console.log('Version mismatch detected:', {
current: this.currentVersionInfo,
stored: getStoredVersionInfo()
});
// Reset dismissed status for version mismatch banner
resetDismissedBanner('version-mismatch');
// Register and show the version mismatch banner
this.registerVersionMismatchBanner();
}
}
} catch (error) {
console.error('Failed to check version info:', error);
}
}
registerVersionMismatchBanner() {
// Get stored and current version for display
const storedVersion = getStoredVersionInfo() || 'unknown';
const currentVersion = this.currentVersionInfo || 'unknown';
bannerService.registerBanner('version-mismatch', {
id: 'version-mismatch',
title: 'Application Update Detected',
content: `Your browser is running an outdated version of LoRA Manager (${storedVersion}). The server has been updated to version ${currentVersion}. Please refresh to ensure proper functionality.`,
actions: [
{
text: 'Refresh Now',
icon: 'fas fa-sync',
action: 'hardRefresh',
type: 'primary'
}
],
dismissible: false,
priority: 10,
countdown: 15,
onRegister: (bannerElement) => {
// Add countdown element
const countdownEl = document.createElement('div');
countdownEl.className = 'banner-countdown';
countdownEl.innerHTML = `<span>Refreshing in <strong>15</strong> seconds...</span>`;
bannerElement.querySelector('.banner-content').appendChild(countdownEl);
// Start countdown
let seconds = 15;
const countdownInterval = setInterval(() => {
seconds--;
const strongEl = countdownEl.querySelector('strong');
if (strongEl) strongEl.textContent = seconds;
if (seconds <= 0) {
clearInterval(countdownInterval);
this.performHardRefresh();
}
}, 1000);
// Store interval ID for cleanup
bannerElement.dataset.countdownInterval = countdownInterval;
// Add action button event handler
const actionBtn = bannerElement.querySelector('.banner-action[data-action="hardRefresh"]');
if (actionBtn) {
actionBtn.addEventListener('click', (e) => {
e.preventDefault();
clearInterval(countdownInterval);
this.performHardRefresh();
});
}
},
onRemove: (bannerElement) => {
// Clear any existing interval
const intervalId = bannerElement.dataset.countdownInterval;
if (intervalId) {
clearInterval(parseInt(intervalId));
}
}
});
}
performHardRefresh() {
// Update stored version info before refreshing
setStoredVersionInfo(this.currentVersionInfo);
// Force a hard refresh by adding cache-busting parameter
const cacheBuster = new Date().getTime();
window.location.href = window.location.pathname +
(window.location.search ? window.location.search + '&' : '?') +
`cache=${cacheBuster}`;
}
}
// Create and export singleton instance

View File

@@ -213,4 +213,45 @@ export function getMapFromStorage(key) {
console.error(`Error loading Map from localStorage (${key}):`, error);
return new Map();
}
}
/**
* Get stored version info from localStorage
* @returns {string|null} The stored version string or null if not found
*/
export function getStoredVersionInfo() {
return getStorageItem('version_info', null);
}
/**
* Store version info to localStorage
* @param {string} versionInfo - The version info string to store
*/
export function setStoredVersionInfo(versionInfo) {
setStorageItem('version_info', versionInfo);
}
/**
* Check if version info matches between stored and current
* @param {string} currentVersionInfo - The current version info from server
* @returns {boolean} True if versions match or no stored version exists
*/
export function isVersionMatch(currentVersionInfo) {
const storedVersion = getStoredVersionInfo();
// If we have no stored version yet, consider it a match
if (storedVersion === null) {
setStoredVersionInfo(currentVersionInfo);
return true;
}
return storedVersion === currentVersionInfo;
}
/**
* Reset the dismissed status of a specific banner
* @param {string} bannerId - The ID of the banner to un-dismiss
*/
export function resetDismissedBanner(bannerId) {
const dismissedBanners = getStorageItem('dismissed_banners', []);
const updatedBanners = dismissedBanners.filter(id => id !== bannerId);
setStorageItem('dismissed_banners', updatedBanners);
}