mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Implement Git-based update functionality with nightly mode support and UI enhancements
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import os
|
||||
import subprocess
|
||||
import aiohttp
|
||||
import logging
|
||||
import toml
|
||||
import subprocess
|
||||
import git
|
||||
from datetime import datetime
|
||||
from aiohttp import web
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +19,7 @@ class UpdateRoutes:
|
||||
"""Register update check routes"""
|
||||
app.router.add_get('/api/check-updates', UpdateRoutes.check_updates)
|
||||
app.router.add_get('/api/version-info', UpdateRoutes.get_version_info)
|
||||
app.router.add_post('/api/perform-update', UpdateRoutes.perform_update)
|
||||
|
||||
@staticmethod
|
||||
async def check_updates(request):
|
||||
@@ -25,6 +28,8 @@ class UpdateRoutes:
|
||||
Returns update status and version information
|
||||
"""
|
||||
try:
|
||||
nightly = request.query.get('nightly', 'false').lower() == 'true'
|
||||
|
||||
# Read local version from pyproject.toml
|
||||
local_version = UpdateRoutes._get_local_version()
|
||||
|
||||
@@ -32,13 +37,21 @@ class UpdateRoutes:
|
||||
git_info = UpdateRoutes._get_git_info()
|
||||
|
||||
# Fetch remote version from GitHub
|
||||
remote_version, changelog = await UpdateRoutes._get_remote_version()
|
||||
if nightly:
|
||||
remote_version, changelog = await UpdateRoutes._get_nightly_version()
|
||||
else:
|
||||
remote_version, changelog = await UpdateRoutes._get_remote_version()
|
||||
|
||||
# Compare versions
|
||||
update_available = UpdateRoutes._compare_versions(
|
||||
local_version.replace('v', ''),
|
||||
remote_version.replace('v', '')
|
||||
)
|
||||
if nightly:
|
||||
# For nightly, compare commit hashes
|
||||
update_available = UpdateRoutes._compare_nightly_versions(git_info, remote_version)
|
||||
else:
|
||||
# For stable, compare semantic versions
|
||||
update_available = UpdateRoutes._compare_versions(
|
||||
local_version.replace('v', ''),
|
||||
remote_version.replace('v', '')
|
||||
)
|
||||
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
@@ -46,7 +59,8 @@ class UpdateRoutes:
|
||||
'latest_version': remote_version,
|
||||
'update_available': update_available,
|
||||
'changelog': changelog,
|
||||
'git_info': git_info
|
||||
'git_info': git_info,
|
||||
'nightly': nightly
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
@@ -55,7 +69,7 @@ class UpdateRoutes:
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
|
||||
@staticmethod
|
||||
async def get_version_info(request):
|
||||
"""
|
||||
@@ -84,6 +98,168 @@ class UpdateRoutes:
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def perform_update(request):
|
||||
"""
|
||||
Perform Git-based update to latest release tag or main branch
|
||||
"""
|
||||
try:
|
||||
# Parse request body
|
||||
body = await request.json() if request.has_body else {}
|
||||
nightly = body.get('nightly', False)
|
||||
|
||||
# Get current plugin directory
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
plugin_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
|
||||
# Backup settings.json if it exists
|
||||
settings_path = os.path.join(plugin_root, 'settings.json')
|
||||
settings_backup = None
|
||||
if os.path.exists(settings_path):
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
settings_backup = f.read()
|
||||
logger.info("Backed up settings.json")
|
||||
|
||||
# Perform Git update
|
||||
success, new_version = await UpdateRoutes._perform_git_update(plugin_root, nightly)
|
||||
|
||||
# Restore settings.json if we backed it up
|
||||
if settings_backup and success:
|
||||
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||
f.write(settings_backup)
|
||||
logger.info("Restored settings.json")
|
||||
|
||||
if success:
|
||||
return web.json_response({
|
||||
'success': True,
|
||||
'message': f'Successfully updated to {new_version}',
|
||||
'new_version': new_version
|
||||
})
|
||||
else:
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': 'Failed to complete Git update'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to perform update: {e}", exc_info=True)
|
||||
return web.json_response({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
async def _get_nightly_version() -> tuple[str, List[str]]:
|
||||
"""
|
||||
Fetch latest commit from main branch
|
||||
"""
|
||||
repo_owner = "willmiao"
|
||||
repo_name = "ComfyUI-Lora-Manager"
|
||||
|
||||
# Use GitHub API to fetch the latest commit from main branch
|
||||
github_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/commits/main"
|
||||
|
||||
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 commit: {response.status}")
|
||||
return "main", []
|
||||
|
||||
data = await response.json()
|
||||
commit_sha = data.get('sha', '')[:7] # Short hash
|
||||
commit_message = data.get('commit', {}).get('message', '')
|
||||
|
||||
# Format as "main-{short_hash}"
|
||||
version = f"main-{commit_sha}"
|
||||
|
||||
# Use commit message as changelog
|
||||
changelog = [commit_message] if commit_message else []
|
||||
|
||||
return version, changelog
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching nightly version: {e}", exc_info=True)
|
||||
return "main", []
|
||||
|
||||
@staticmethod
|
||||
def _compare_nightly_versions(local_git_info: Dict[str, str], remote_version: str) -> bool:
|
||||
"""
|
||||
Compare local commit hash with remote main branch
|
||||
"""
|
||||
try:
|
||||
local_hash = local_git_info.get('short_hash', 'unknown')
|
||||
if local_hash == 'unknown':
|
||||
return True # Assume update available if we can't get local hash
|
||||
|
||||
# Extract remote hash from version string (format: "main-{hash}")
|
||||
if '-' in remote_version:
|
||||
remote_hash = remote_version.split('-')[-1]
|
||||
return local_hash != remote_hash
|
||||
|
||||
return True # Default to update available
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error comparing nightly versions: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _perform_git_update(plugin_root: str, nightly: bool = False) -> tuple[bool, str]:
|
||||
"""
|
||||
Perform Git-based update using GitPython
|
||||
|
||||
Args:
|
||||
plugin_root: Path to the plugin root directory
|
||||
nightly: Whether to update to main branch or latest release
|
||||
|
||||
Returns:
|
||||
tuple: (success, new_version)
|
||||
"""
|
||||
try:
|
||||
# Open the Git repository
|
||||
repo = git.Repo(plugin_root)
|
||||
|
||||
# Fetch latest changes
|
||||
origin = repo.remotes.origin
|
||||
origin.fetch()
|
||||
|
||||
if nightly:
|
||||
# Switch to main branch and pull latest
|
||||
main_branch = 'main'
|
||||
if main_branch not in [branch.name for branch in repo.branches]:
|
||||
# Create local main branch if it doesn't exist
|
||||
repo.create_head(main_branch, origin.refs.main)
|
||||
|
||||
repo.heads[main_branch].checkout()
|
||||
origin.pull(main_branch)
|
||||
|
||||
# Get new commit hash
|
||||
new_version = f"main-{repo.head.commit.hexsha[:7]}"
|
||||
|
||||
else:
|
||||
# Get latest release tag
|
||||
tags = sorted(repo.tags, key=lambda t: t.commit.committed_datetime, reverse=True)
|
||||
if not tags:
|
||||
logger.error("No tags found in repository")
|
||||
return False, ""
|
||||
|
||||
latest_tag = tags[0]
|
||||
|
||||
# Checkout to latest tag
|
||||
repo.git.checkout(latest_tag.name)
|
||||
|
||||
new_version = latest_tag.name
|
||||
|
||||
logger.info(f"Successfully updated to {new_version}")
|
||||
return True, new_version
|
||||
|
||||
except git.exc.GitError as e:
|
||||
logger.error(f"Git error during update: {e}")
|
||||
return False, ""
|
||||
except Exception as e:
|
||||
logger.error(f"Error during Git update: {e}")
|
||||
return False, ""
|
||||
|
||||
@staticmethod
|
||||
def _get_local_version() -> str:
|
||||
"""Get local plugin version from pyproject.toml"""
|
||||
|
||||
@@ -14,7 +14,8 @@ dependencies = [
|
||||
"requests",
|
||||
"toml",
|
||||
"natsort",
|
||||
"msgpack"
|
||||
"msgpack",
|
||||
"GitPython"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -11,3 +11,4 @@ numpy
|
||||
natsort
|
||||
msgpack
|
||||
pyyaml
|
||||
GitPython
|
||||
|
||||
@@ -50,8 +50,8 @@ html, body {
|
||||
--lora-border: oklch(90% 0.02 256 / 0.15);
|
||||
--lora-text: oklch(95% 0.02 256);
|
||||
--lora-error: oklch(75% 0.32 29);
|
||||
--lora-warning: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h)); /* Modified to be used with oklch() */
|
||||
--lora-success: oklch(var(--lora-success-l) var(--lora-success-c) var(--lora-success-h)); /* New green success color */
|
||||
--lora-warning: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
|
||||
--lora-success: oklch(var(--lora-success-l) var(--lora-success-c) var(--lora-success-h));
|
||||
|
||||
/* Spacing Scale */
|
||||
--space-1: calc(8px * 1);
|
||||
|
||||
@@ -223,11 +223,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.update-badge.hidden,
|
||||
.update-badge:not(.visible) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.app-title {
|
||||
|
||||
@@ -172,6 +172,91 @@ body.modal-open {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Update Modal specific styles */
|
||||
.update-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.update-link {
|
||||
color: var(--lora-accent);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.update-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Update progress styles */
|
||||
.update-progress {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: var(--space-2);
|
||||
margin: var(--space-2) 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .update-progress {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .progress-bar {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--lora-accent);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Update button states */
|
||||
#updateBtn {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
#updateBtn.updating {
|
||||
background-color: var(--lora-warning);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#updateBtn.success {
|
||||
background-color: var(--lora-success);
|
||||
}
|
||||
|
||||
#updateBtn.error {
|
||||
background-color: var(--lora-error);
|
||||
}
|
||||
|
||||
/* Settings styles */
|
||||
.settings-toggle {
|
||||
width: 36px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
|
||||
export class UpdateService {
|
||||
constructor() {
|
||||
this.updateCheckInterval = 24 * 60 * 60 * 1000; // 24 hours
|
||||
this.updateCheckInterval = 60 * 60 * 1000; // 1 hour
|
||||
this.currentVersion = "v0.0.0"; // Initialize with default values
|
||||
this.latestVersion = "v0.0.0"; // Initialize with default values
|
||||
this.updateInfo = null;
|
||||
@@ -13,8 +13,10 @@ export class UpdateService {
|
||||
branch: "unknown",
|
||||
commit_date: "unknown"
|
||||
};
|
||||
this.updateNotificationsEnabled = getStorageItem('show_update_notifications');
|
||||
this.updateNotificationsEnabled = getStorageItem('show_update_notifications', true);
|
||||
this.lastCheckTime = parseInt(getStorageItem('last_update_check') || '0');
|
||||
this.isUpdating = false;
|
||||
this.nightlyMode = getStorageItem('nightly_updates', false);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -28,23 +30,44 @@ export class UpdateService {
|
||||
this.updateBadgeVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
const updateBtn = document.getElementById('updateBtn');
|
||||
if (updateBtn) {
|
||||
updateBtn.addEventListener('click', () => this.performUpdate());
|
||||
}
|
||||
|
||||
// Register event listener for nightly update toggle
|
||||
const nightlyCheckbox = document.getElementById('nightlyUpdateToggle');
|
||||
if (nightlyCheckbox) {
|
||||
nightlyCheckbox.checked = this.nightlyMode;
|
||||
nightlyCheckbox.addEventListener('change', (e) => {
|
||||
this.nightlyMode = e.target.checked;
|
||||
setStorageItem('nightly_updates', e.target.checked);
|
||||
this.updateNightlyWarning();
|
||||
this.updateModalContent();
|
||||
// Re-check for updates when switching channels
|
||||
this.manualCheckForUpdates();
|
||||
});
|
||||
this.updateNightlyWarning();
|
||||
}
|
||||
|
||||
// Perform update check if needed
|
||||
this.checkForUpdates().then(() => {
|
||||
// Ensure badges are updated after checking
|
||||
this.updateBadgeVisibility();
|
||||
});
|
||||
|
||||
// Set up event listener for update button
|
||||
// const updateToggle = document.getElementById('updateToggleBtn');
|
||||
// if (updateToggle) {
|
||||
// updateToggle.addEventListener('click', () => this.toggleUpdateModal());
|
||||
// }
|
||||
|
||||
// Immediately update modal content with current values (even if from default)
|
||||
this.updateModalContent();
|
||||
}
|
||||
|
||||
updateNightlyWarning() {
|
||||
const warning = document.getElementById('nightlyWarning');
|
||||
if (warning) {
|
||||
warning.style.display = this.nightlyMode ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdates() {
|
||||
// Check if we should perform an update check
|
||||
const now = Date.now();
|
||||
@@ -59,8 +82,8 @@ export class UpdateService {
|
||||
}
|
||||
|
||||
try {
|
||||
// Call backend API to check for updates
|
||||
const response = await fetch('/api/check-updates');
|
||||
// Call backend API to check for updates with nightly flag
|
||||
const response = await fetch(`/api/check-updates?nightly=${this.nightlyMode}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
@@ -137,8 +160,8 @@ export class UpdateService {
|
||||
const shouldShow = this.updateNotificationsEnabled && this.updateAvailable;
|
||||
|
||||
if (updateBadge) {
|
||||
updateBadge.classList.toggle('hidden', !shouldShow);
|
||||
console.log("Update badge visibility:", !shouldShow ? "hidden" : "visible");
|
||||
updateBadge.classList.toggle('visible', shouldShow);
|
||||
console.log("Update badge visibility:", shouldShow ? "visible" : "hidden");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +180,17 @@ export class UpdateService {
|
||||
const newVersionEl = modal.querySelector('.new-version .version-number');
|
||||
|
||||
if (currentVersionEl) currentVersionEl.textContent = this.currentVersion;
|
||||
if (newVersionEl) newVersionEl.textContent = this.latestVersion;
|
||||
|
||||
if (newVersionEl) {
|
||||
newVersionEl.textContent = this.latestVersion;
|
||||
}
|
||||
|
||||
// Update update button state
|
||||
const updateBtn = modal.querySelector('#updateBtn');
|
||||
if (updateBtn) {
|
||||
updateBtn.classList.toggle('disabled', !this.updateAvailable || this.isUpdating);
|
||||
updateBtn.disabled = !this.updateAvailable || this.isUpdating;
|
||||
}
|
||||
|
||||
// Update git info
|
||||
const gitInfoEl = modal.querySelector('.git-info');
|
||||
@@ -218,6 +251,131 @@ export class UpdateService {
|
||||
}
|
||||
}
|
||||
|
||||
async performUpdate() {
|
||||
if (!this.updateAvailable || this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isUpdating = true;
|
||||
this.updateUpdateUI('updating', 'Updating...');
|
||||
this.showUpdateProgress(true);
|
||||
|
||||
// Update progress
|
||||
this.updateProgress(10, 'Preparing update...');
|
||||
|
||||
const response = await fetch('/api/perform-update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nightly: this.nightlyMode
|
||||
})
|
||||
});
|
||||
|
||||
this.updateProgress(50, 'Installing update...');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgress(100, 'Update completed successfully!');
|
||||
this.updateUpdateUI('success', 'Updated!');
|
||||
|
||||
// Show success message and suggest restart
|
||||
setTimeout(() => {
|
||||
this.showUpdateCompleteMessage(data.new_version);
|
||||
}, 1000);
|
||||
|
||||
} else {
|
||||
throw new Error(data.error || 'Update failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Update failed:', error);
|
||||
this.updateUpdateUI('error', 'Update Failed');
|
||||
this.updateProgress(0, `Update failed: ${error.message}`);
|
||||
|
||||
// Hide progress after error
|
||||
setTimeout(() => {
|
||||
this.showUpdateProgress(false);
|
||||
}, 3000);
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateUpdateUI(state, text) {
|
||||
const updateBtn = document.getElementById('updateBtn');
|
||||
const updateBtnText = document.getElementById('updateBtnText');
|
||||
|
||||
if (updateBtn && updateBtnText) {
|
||||
// Remove existing state classes
|
||||
updateBtn.classList.remove('updating', 'success', 'error', 'disabled');
|
||||
|
||||
// Add new state class
|
||||
if (state !== 'normal') {
|
||||
updateBtn.classList.add(state);
|
||||
}
|
||||
|
||||
// Update button text
|
||||
updateBtnText.textContent = text;
|
||||
|
||||
// Update disabled state
|
||||
updateBtn.disabled = (state === 'updating' || state === 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
showUpdateProgress(show) {
|
||||
const progressContainer = document.getElementById('updateProgress');
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress(percentage, text) {
|
||||
const progressFill = document.getElementById('updateProgressFill');
|
||||
const progressText = document.getElementById('updateProgressText');
|
||||
|
||||
if (progressFill) {
|
||||
progressFill.style.width = `${percentage}%`;
|
||||
}
|
||||
|
||||
if (progressText) {
|
||||
progressText.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
showUpdateCompleteMessage(newVersion) {
|
||||
const modal = document.getElementById('updateModal');
|
||||
if (!modal) return;
|
||||
|
||||
// Update the modal content to show completion
|
||||
const progressText = document.getElementById('updateProgressText');
|
||||
if (progressText) {
|
||||
progressText.innerHTML = `
|
||||
<div style="text-align: center; color: var(--lora-success);">
|
||||
<i class="fas fa-check-circle" style="margin-right: 8px;"></i>
|
||||
Successfully updated to ${newVersion}!
|
||||
<br><br>
|
||||
<small style="opacity: 0.8;">
|
||||
Please restart ComfyUI to complete the update process.
|
||||
</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update current version display
|
||||
this.currentVersion = newVersion;
|
||||
this.updateAvailable = false;
|
||||
|
||||
// Refresh the modal content
|
||||
setTimeout(() => {
|
||||
this.updateModalContent();
|
||||
this.showUpdateProgress(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Simple markdown parser for changelog items
|
||||
parseMarkdown(text) {
|
||||
if (!text) return '';
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div class="update-toggle" id="updateToggleBtn" title="Check Updates">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="update-badge hidden"></span>
|
||||
<span class="update-badge"></span>
|
||||
</div>
|
||||
<div class="support-toggle" id="supportToggleBtn" title="Support">
|
||||
<i class="fas fa-heart"></i>
|
||||
|
||||
@@ -476,9 +476,26 @@
|
||||
<span class="version-number">v0.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager" target="_blank" class="update-link">
|
||||
<i class="fas fa-external-link-alt"></i> View on GitHub
|
||||
</a>
|
||||
|
||||
<div class="update-actions">
|
||||
<a href="https://github.com/willmiao/ComfyUI-Lora-Manager" target="_blank" class="update-link">
|
||||
<i class="fas fa-external-link-alt"></i> View on GitHub
|
||||
</a>
|
||||
<button id="updateBtn" class="primary-btn disabled">
|
||||
<i class="fas fa-download"></i>
|
||||
<span id="updateBtnText">Update Now</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Progress Section -->
|
||||
<div class="update-progress" id="updateProgress" style="display: none;">
|
||||
<div class="progress-info">
|
||||
<div class="progress-text" id="updateProgressText">Preparing update...</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="updateProgressFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="changelog-section">
|
||||
|
||||
Reference in New Issue
Block a user