diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index cd66783d..22cf2757 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -13,6 +13,7 @@ from operator import itemgetter from ..services.websocket_manager import ws_manager from ..services.settings_manager import settings import asyncio +from .update_routes import UpdateRoutes logger = logging.getLogger(__name__) @@ -43,6 +44,9 @@ class ApiRoutes: app.router.add_post('/loras/api/save-metadata', routes.save_metadata) app.router.add_get('/api/lora-preview-url', routes.get_lora_preview_url) # Add new route + # Add update check routes + UpdateRoutes.setup_routes(app) + async def delete_model(self, request: web.Request) -> web.Response: """Handle model deletion request""" try: diff --git a/py/routes/update_routes.py b/py/routes/update_routes.py new file mode 100644 index 00000000..41037aae --- /dev/null +++ b/py/routes/update_routes.py @@ -0,0 +1,180 @@ +import os +import aiohttp +import logging +import toml +from aiohttp import web +from typing import Dict, Any, List + +logger = logging.getLogger(__name__) + +class UpdateRoutes: + """Routes for handling plugin update checks""" + + @staticmethod + def setup_routes(app): + """Register update check routes""" + app.router.add_get('/loras/api/check-updates', UpdateRoutes.check_updates) + + @staticmethod + async def check_updates(request): + """ + Check for plugin updates by comparing local version with GitHub + Returns update status and version information + """ + try: + # Read local version from pyproject.toml + local_version = UpdateRoutes._get_local_version() + logger.info(f"Local version: {local_version}") + + # Fetch remote version from GitHub + remote_version, changelog = await UpdateRoutes._get_remote_version() + logger.info(f"Remote version: {remote_version}") + + # Compare versions + update_available = UpdateRoutes._compare_versions( + local_version.replace('v', ''), + remote_version.replace('v', '') + ) + + logger.info(f"Update available: {update_available}") + + return web.json_response({ + 'success': True, + 'current_version': local_version, + 'latest_version': remote_version, + 'update_available': update_available, + 'changelog': changelog + }) + + except Exception as e: + logger.error(f"Failed to check for updates: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }) + + @staticmethod + def _get_local_version() -> str: + """Get local plugin version from pyproject.toml""" + try: + # Find the plugin's pyproject.toml file + current_dir = os.path.dirname(os.path.abspath(__file__)) + plugin_root = os.path.dirname(os.path.dirname(current_dir)) + pyproject_path = os.path.join(plugin_root, 'pyproject.toml') + + # Read and parse the toml file + if os.path.exists(pyproject_path): + with open(pyproject_path, 'r', encoding='utf-8') as f: + project_data = toml.load(f) + version = project_data.get('project', {}).get('version', '0.0.0') + return f"v{version}" + else: + logger.warning(f"pyproject.toml not found at {pyproject_path}") + return "v0.0.0" + + except Exception as e: + logger.error(f"Failed to get local version: {e}", exc_info=True) + return "v0.0.0" + + @staticmethod + async def _get_remote_version() -> tuple[str, List[str]]: + """ + Fetch remote version from GitHub + Returns: + tuple: (version string, changelog list) + """ + repo_owner = "willmiao" + repo_name = "ComfyUI-Lora-Manager" + + # Use GitHub API to fetch the latest release + github_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(github_url, headers={'Accept': 'application/vnd.github+json'}) as response: + if response.status != 200: + logger.warning(f"Failed to fetch GitHub release: {response.status}") + return "v0.0.0", [] + + data = await response.json() + version = data.get('tag_name', '') + if not version.startswith('v'): + version = f"v{version}" + + # Extract changelog from release notes + body = data.get('body', '') + changelog = UpdateRoutes._parse_changelog(body) + + return version, changelog + + except Exception as e: + logger.error(f"Error fetching remote version: {e}", exc_info=True) + return "v0.0.0", [] + + @staticmethod + def _parse_changelog(release_notes: str) -> List[str]: + """ + Parse GitHub release notes to extract changelog items + + Args: + release_notes: GitHub release notes markdown text + + Returns: + List of changelog items + """ + changelog = [] + + # Simple parsing - extract bullet points + lines = release_notes.split('\n') + for line in lines: + line = line.strip() + # Look for bullet points or numbered items + if line.startswith('- ') or line.startswith('* '): + item = line[2:].strip() + if item: + changelog.append(item) + # Match numbered items like "1. Item" + elif len(line) > 2 and line[0].isdigit() and line[1:].startswith('. '): + item = line[line.index('. ')+2:].strip() + if item: + changelog.append(item) + + # If we couldn't parse specific items, use the whole text (limited) + if not changelog and release_notes: + # Limit to first 500 chars and add ellipsis + summary = release_notes.strip()[:500] + if len(release_notes) > 500: + summary += "..." + changelog.append(summary) + + return changelog + + @staticmethod + def _compare_versions(version1: str, version2: str) -> bool: + """ + Compare two semantic version strings + Returns True if version2 is newer than version1 + """ + try: + # Split versions into components + v1_parts = [int(x) for x in version1.split('.')] + v2_parts = [int(x) for x in version2.split('.')] + + # Ensure both have 3 components (major.minor.patch) + while len(v1_parts) < 3: + v1_parts.append(0) + while len(v2_parts) < 3: + v2_parts.append(0) + + # Compare version components + for i in range(3): + if v2_parts[i] > v1_parts[i]: + return True + elif v2_parts[i] < v1_parts[i]: + return False + + # Versions are equal + return False + except Exception as e: + logger.error(f"Error comparing versions: {e}", exc_info=True) + return False diff --git a/static/css/components/modal.css b/static/css/components/modal.css index 4f318f37..979cf2cc 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -1224,14 +1224,14 @@ body.modal-open { transform: translateY(-2px); } -.changelog-section, .update-instructions { +.changelog-section { background: var(--bg-color); border: 1px solid var(--lora-border); border-radius: var(--border-radius-sm); padding: var(--space-2); } -.changelog-section h3, .update-instructions h3 { +.changelog-section h3 { margin-top: 0; margin-bottom: var(--space-2); color: var(--lora-accent); @@ -1239,7 +1239,7 @@ body.modal-open { } .changelog-content { - max-height: 200px; + max-height: 300px; /* Increased height since we removed instructions */ overflow-y: auto; } @@ -1272,17 +1272,6 @@ body.modal-open { color: var(--text-color); } -.code-block { - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-xs); - padding: var(--space-1) var(--space-2); - margin: var(--space-1) 0; - font-family: monospace; - color: var(--text-color); - overflow-x: auto; -} - @media (max-width: 480px) { .update-info { flex-direction: column; @@ -1353,10 +1342,4 @@ input:checked + .toggle-slider:before { color: var(--text-color); } -.preference-note { - margin-top: 4px; - font-size: 0.8em; - color: var(--text-color); - opacity: 0.7; - margin-left: 52px; -} \ No newline at end of file +/* ...existing code... */ \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 7c767553..de4189a8 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,6 +1,7 @@ import { debounce } from './utils/debounce.js'; import { LoadingManager } from './managers/LoadingManager.js'; import { modalManager } from './managers/ModalManager.js'; +import { updateService } from './managers/UpdateService.js'; import { state } from './state/index.js'; import { showLoraModal, toggleShowcase, scrollToTop } from './components/LoraCard.js'; import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js'; @@ -50,9 +51,10 @@ window.toggleShowcase = toggleShowcase; window.scrollToTop = scrollToTop; // Initialize everything when DOM is ready -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { state.loadingManager = new LoadingManager(); modalManager.initialize(); // Initialize modalManager after DOM is loaded + updateService.initialize(); // Initialize updateService after modalManager window.downloadManager = new DownloadManager(); // Move this after modalManager initialization window.filterManager = new FilterManager(); // Initialize filter manager diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index 5f0077eb..a513ef98 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -2,7 +2,6 @@ export class ModalManager { constructor() { this.modals = new Map(); this.scrollPosition = 0; - this.updateAvailable = false; } initialize() { @@ -75,9 +74,8 @@ export class ModalManager { document.addEventListener('keydown', this.boundHandleEscape); this.initialized = true; - // Initialize corner controls and update modal + // Initialize corner controls this.initCornerControls(); - this.initUpdateModal(); } registerModal(id, config) { @@ -159,79 +157,16 @@ export class ModalManager { } } - // Add method to initialize corner controls behavior + // Keep only the corner controls initialization initCornerControls() { const cornerControls = document.querySelector('.corner-controls'); const cornerControlsToggle = document.querySelector('.corner-controls-toggle'); if(cornerControls && cornerControlsToggle) { - // Check for updates (mock implementation) - this.checkForUpdates(); - - // Apply the initial badge state based on localStorage - const showUpdates = localStorage.getItem('show_update_notifications'); - if (showUpdates === 'false') { - this.updateBadgeVisibility(false); - } - } - } - - // Modified update checker - checkForUpdates() { - // First check if user has disabled update notifications - const showUpdates = localStorage.getItem('show_update_notifications'); - - // For demo purposes, we'll simulate an update being available - setTimeout(() => { - // We have an update available (mock) - this.updateAvailable = true; - - // Only show badges if notifications are enabled - const shouldShow = showUpdates !== 'false'; - this.updateBadgeVisibility(shouldShow); - }, 2000); - } - - // Add method to initialize update modal - initUpdateModal() { - const updateModal = document.getElementById('updateModal'); - if (!updateModal) return; - - const checkbox = updateModal.querySelector('#updateNotifications'); - if (!checkbox) return; - - // Set initial state from localStorage or default to true - const showUpdates = localStorage.getItem('show_update_notifications'); - checkbox.checked = showUpdates === null || showUpdates === 'true'; - - // Apply the initial badge visibility based on checkbox state - this.updateBadgeVisibility(checkbox.checked); - - // Add event listener for changes - checkbox.addEventListener('change', (e) => { - localStorage.setItem('show_update_notifications', e.target.checked); - - // Immediately update badge visibility based on the new setting - this.updateBadgeVisibility(e.target.checked); - }); - } - - // Enhanced helper method to update badge visibility - updateBadgeVisibility(show) { - const updateToggle = document.querySelector('.update-toggle'); - const updateBadge = document.querySelector('.update-toggle .update-badge'); - const cornerBadge = document.querySelector('.corner-badge'); - - if (updateToggle) { - updateToggle.title = show && this.updateAvailable ? "Update Available" : "Check Updates"; - } - - if (updateBadge) { - updateBadge.classList.toggle('hidden', !(show && this.updateAvailable)); - } - - if (cornerBadge) { - cornerBadge.classList.toggle('hidden', !(show && this.updateAvailable)); + // Toggle corner controls visibility + cornerControlsToggle.addEventListener('click', () => { + cornerControls.classList.toggle('expanded'); + }); } } } diff --git a/static/js/managers/UpdateService.js b/static/js/managers/UpdateService.js new file mode 100644 index 00000000..7ced9257 --- /dev/null +++ b/static/js/managers/UpdateService.js @@ -0,0 +1,183 @@ +import { modalManager } from './ModalManager.js'; + +export class UpdateService { + constructor() { + this.updateCheckInterval = 24 * 60 * 60 * 1000; // 24 hours + this.currentVersion = "v0.0.0"; // Initialize with default values + this.latestVersion = "v0.0.0"; // Initialize with default values + this.updateInfo = null; + this.updateAvailable = false; + this.updateNotificationsEnabled = localStorage.getItem('show_update_notifications') !== 'false'; + this.lastCheckTime = parseInt(localStorage.getItem('last_update_check') || '0'); + } + + initialize() { + // Initialize update preferences from localStorage + const showUpdates = localStorage.getItem('show_update_notifications'); + this.updateNotificationsEnabled = showUpdates === null || showUpdates === 'true'; + + // Register event listener for update notification toggle + const updateCheckbox = document.getElementById('updateNotifications'); + if (updateCheckbox) { + updateCheckbox.checked = this.updateNotificationsEnabled; + updateCheckbox.addEventListener('change', (e) => { + this.updateNotificationsEnabled = e.target.checked; + localStorage.setItem('show_update_notifications', e.target.checked); + this.updateBadgeVisibility(); + }); + } + + // Perform update check if needed + this.checkForUpdates(); + + // Set up event listener for update button + const updateToggle = document.querySelector('.update-toggle'); + if (updateToggle) { + updateToggle.addEventListener('click', () => this.showUpdateModal()); + } + + // Immediately update modal content with current values (even if from default) + this.updateModalContent(); + } + + async checkForUpdates() { + // Check if we should perform an update check + const now = Date.now(); + if (now - this.lastCheckTime < this.updateCheckInterval) { + // If we already have update info, just update the UI + if (this.updateAvailable) { + this.updateBadgeVisibility(); + } + return; + } + + try { + // Call backend API to check for updates + const response = await fetch('/loras/api/check-updates'); + const data = await response.json(); + + if (data.success) { + this.currentVersion = data.current_version || "v0.0.0"; + this.latestVersion = data.latest_version || "v0.0.0"; + this.updateInfo = data; + + // Determine if update is available + this.updateAvailable = data.update_available; + + // Update last check time + this.lastCheckTime = now; + localStorage.setItem('last_update_check', now.toString()); + + // Update UI + this.updateBadgeVisibility(); + this.updateModalContent(); + + console.log("Update check complete:", { + currentVersion: this.currentVersion, + latestVersion: this.latestVersion, + updateAvailable: this.updateAvailable + }); + } + } catch (error) { + console.error('Failed to check for updates:', error); + } + } + + updateBadgeVisibility() { + const updateToggle = document.querySelector('.update-toggle'); + const updateBadge = document.querySelector('.update-toggle .update-badge'); + const cornerBadge = document.querySelector('.corner-badge'); + + if (updateToggle) { + updateToggle.title = this.updateNotificationsEnabled && this.updateAvailable + ? "Update Available" + : "Check Updates"; + } + + if (updateBadge) { + const shouldShow = this.updateNotificationsEnabled && this.updateAvailable; + updateBadge.classList.toggle('hidden', !shouldShow); + } + + if (cornerBadge) { + const shouldShow = this.updateNotificationsEnabled && this.updateAvailable; + cornerBadge.classList.toggle('hidden', !shouldShow); + } + } + + updateModalContent() { + const modal = document.getElementById('updateModal'); + if (!modal) return; + + // Update title based on update availability + const headerTitle = modal.querySelector('.update-header h2'); + if (headerTitle) { + headerTitle.textContent = this.updateAvailable ? "Update Available" : "Check for Updates"; + } + + // Always update version information, even if updateInfo is null + const currentVersionEl = modal.querySelector('.current-version .version-number'); + const newVersionEl = modal.querySelector('.new-version .version-number'); + + if (currentVersionEl) currentVersionEl.textContent = this.currentVersion; + if (newVersionEl) newVersionEl.textContent = this.latestVersion; + + // Update changelog content if available + if (this.updateInfo && this.updateInfo.changelog) { + const changelogContent = modal.querySelector('.changelog-content'); + if (changelogContent) { + changelogContent.innerHTML = ''; // Clear existing content + + // Create changelog item + const changelogItem = document.createElement('div'); + changelogItem.className = 'changelog-item'; + + const versionHeader = document.createElement('h4'); + versionHeader.textContent = `Version ${this.latestVersion}`; + changelogItem.appendChild(versionHeader); + + // Create changelog list + const changelogList = document.createElement('ul'); + + if (this.updateInfo.changelog && this.updateInfo.changelog.length > 0) { + this.updateInfo.changelog.forEach(item => { + const listItem = document.createElement('li'); + listItem.textContent = item; + changelogList.appendChild(listItem); + }); + } else { + // If no changelog items available + const listItem = document.createElement('li'); + listItem.textContent = "No detailed changelog available. Check GitHub for more information."; + changelogList.appendChild(listItem); + } + + changelogItem.appendChild(changelogList); + changelogContent.appendChild(changelogItem); + } + } + + // Update GitHub link to point to the specific release if available + const githubLink = modal.querySelector('.update-link'); + if (githubLink && this.latestVersion) { + const versionTag = this.latestVersion.replace(/^v/, ''); + githubLink.href = `https://github.com/willmiao/ComfyUI-Lora-Manager/releases/tag/v${versionTag}`; + } + } + + showUpdateModal() { + // Force a check for updates when showing the modal + this.manualCheckForUpdates().then(() => { + // Show the modal after update check completes + modalManager.showModal('updateModal'); + }); + } + + async manualCheckForUpdates() { + this.lastCheckTime = 0; // Reset last check time to force check + await this.checkForUpdates(); + } +} + +// Create and export singleton instance +export const updateService = new UpdateService(); diff --git a/templates/components/modals.html b/templates/components/modals.html index bef795b8..d176b9a7 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -223,18 +223,18 @@
-

Update Available

+

Check for Updates

Current Version: - v1.0.0 + v0.0.0
New Version: - v1.1.0 + v0.0.0
@@ -245,27 +245,14 @@

Changelog

+
-

Version 1.1.0

-
    -
  • Added support for custom folder structures
  • -
  • Improved search functionality
  • -
  • Fixed bug with loading large libraries
  • -
  • Added new sorting options
  • -
+

Checking for updates...

+

Please wait while we check for the latest version.

-
-

How to Update

-

To update LoRA Manager, run the following command in your ComfyUI directory:

-
- git pull origin main -
-

Or update through the ComfyUI Manager if you installed it that way.

-
-