mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-09 20:39:25 -03:00
feat(update): add per-folder update check via sidebar context menu (#944)
This commit is contained in:
@@ -745,3 +745,8 @@
|
||||
.sidebar-tree-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Folder context menu - positioned relative to sidebar */
|
||||
#sidebarFolderContextMenu {
|
||||
z-index: var(--z-modal, 1002);
|
||||
}
|
||||
|
||||
@@ -766,6 +766,49 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshUpdatesForFolder(folderPath, { force = false } = {}) {
|
||||
if (!folderPath) {
|
||||
throw new Error('No folder path provided');
|
||||
}
|
||||
|
||||
try {
|
||||
state.loadingManager.show('Checking for updates...', 0);
|
||||
state.loadingManager.showCancelButton(() => this.cancelTask());
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
folder_path: folderPath,
|
||||
force
|
||||
})
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (error) {
|
||||
console.warn('Unable to parse refresh updates response as JSON', error);
|
||||
}
|
||||
|
||||
if (!response.ok || payload?.success !== true) {
|
||||
if (payload?.status === 'cancelled') {
|
||||
showToast('toast.api.operationCancelled', {}, 'info');
|
||||
return null;
|
||||
}
|
||||
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing updates for folder:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCivitaiVersions(modelId, source = null) {
|
||||
try {
|
||||
let requestUrl = `${this.apiConfig.endpoints.civitaiVersions}/${modelId}`;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { translate } from '../utils/i18nHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { bulkManager } from '../managers/BulkManager.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
|
||||
import { escapeHtml, escapeAttribute } from './shared/utils.js';
|
||||
|
||||
export class SidebarManager {
|
||||
@@ -41,6 +42,7 @@ export class SidebarManager {
|
||||
|
||||
// Bind methods
|
||||
this.handleTreeClick = this.handleTreeClick.bind(this);
|
||||
this.handleTreeContextMenu = this.handleTreeContextMenu.bind(this);
|
||||
this.handleBreadcrumbClick = this.handleBreadcrumbClick.bind(this);
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleSidebarHeaderClick = this.handleSidebarHeaderClick.bind(this);
|
||||
@@ -185,6 +187,8 @@ export class SidebarManager {
|
||||
}
|
||||
if (folderTree) {
|
||||
folderTree.removeEventListener('click', this.handleTreeClick);
|
||||
folderTree.removeEventListener('contextmenu', this.handleTreeContextMenu);
|
||||
folderTree.removeEventListener('dragover', this.handleFolderDragOver);
|
||||
}
|
||||
if (sidebarBreadcrumbNav) {
|
||||
sidebarBreadcrumbNav.removeEventListener('click', this.handleBreadcrumbClick);
|
||||
@@ -977,6 +981,7 @@ export class SidebarManager {
|
||||
const folderTree = document.getElementById('sidebarFolderTree');
|
||||
if (folderTree) {
|
||||
folderTree.addEventListener('click', this.handleTreeClick);
|
||||
folderTree.addEventListener('contextmenu', this.handleTreeContextMenu);
|
||||
}
|
||||
|
||||
// Breadcrumb click handler
|
||||
@@ -1027,6 +1032,19 @@ export class SidebarManager {
|
||||
if (displayModeToggleBtn) {
|
||||
displayModeToggleBtn.addEventListener('click', this.handleDisplayModeToggle);
|
||||
}
|
||||
|
||||
// Sidebar folder context menu click handler
|
||||
const sidebarFolderMenu = document.getElementById('sidebarFolderContextMenu');
|
||||
if (sidebarFolderMenu) {
|
||||
sidebarFolderMenu.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.context-menu-item');
|
||||
if (!item) return;
|
||||
const action = item.dataset.action;
|
||||
if (action) {
|
||||
this.handleFolderContextMenuAction(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentClick(event) {
|
||||
@@ -1398,6 +1416,82 @@ export class SidebarManager {
|
||||
}
|
||||
}
|
||||
|
||||
handleTreeContextMenu(event) {
|
||||
const nodeContent = event.target.closest('.sidebar-tree-node, .sidebar-folder-item');
|
||||
if (!nodeContent) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const path = nodeContent.dataset.path;
|
||||
if (path === undefined || path === null || path === '') return;
|
||||
|
||||
this._showFolderContextMenu(event.clientX, event.clientY, path);
|
||||
}
|
||||
|
||||
_showFolderContextMenu(x, y, path) {
|
||||
this._closeFolderContextMenu();
|
||||
|
||||
const menu = document.getElementById('sidebarFolderContextMenu');
|
||||
if (!menu) return;
|
||||
|
||||
menu.style.left = `${x}px`;
|
||||
menu.style.top = `${y}px`;
|
||||
menu.style.display = 'block';
|
||||
menu.dataset.folderPath = path;
|
||||
|
||||
this._folderContextOpen = true;
|
||||
|
||||
// Close on next click outside
|
||||
this._folderContextCloseHandler = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
this._closeFolderContextMenu();
|
||||
}
|
||||
};
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this._folderContextCloseHandler);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_closeFolderContextMenu() {
|
||||
const menu = document.getElementById('sidebarFolderContextMenu');
|
||||
if (menu) {
|
||||
menu.style.display = 'none';
|
||||
delete menu.dataset.folderPath;
|
||||
}
|
||||
if (this._folderContextCloseHandler) {
|
||||
document.removeEventListener('click', this._folderContextCloseHandler);
|
||||
this._folderContextCloseHandler = null;
|
||||
}
|
||||
this._folderContextOpen = false;
|
||||
}
|
||||
|
||||
handleFolderContextMenuAction(action) {
|
||||
const menu = document.getElementById('sidebarFolderContextMenu');
|
||||
if (!menu) return;
|
||||
|
||||
const path = menu.dataset.folderPath;
|
||||
this._closeFolderContextMenu();
|
||||
|
||||
if (!path) return;
|
||||
|
||||
this._performFolderAction(action, path);
|
||||
}
|
||||
|
||||
async _performFolderAction(action, path) {
|
||||
switch (action) {
|
||||
case 'check-folder-updates':
|
||||
try {
|
||||
await performFolderUpdateCheck(path);
|
||||
} catch (error) {
|
||||
console.error('Folder update check failed:', error);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.warn('Unknown folder action:', action);
|
||||
}
|
||||
}
|
||||
|
||||
handleBreadcrumbClick(event) {
|
||||
const breadcrumbItem = event.target.closest('.sidebar-breadcrumb-item');
|
||||
const dropdownItem = event.target.closest('.breadcrumb-dropdown-item');
|
||||
|
||||
@@ -100,6 +100,90 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
|
||||
return { status, displayName, records, error };
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a model update check scoped to a specific folder.
|
||||
* @param {string} folderPath - The relative folder path to check.
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.onComplete] - Callback invoked after the request settles.
|
||||
* @returns {Promise<{status: string, records: Array, error: Error | null}>}
|
||||
*/
|
||||
export async function performFolderUpdateCheck(folderPath, { onComplete } = {}) {
|
||||
const modelType = getCurrentModelType();
|
||||
const apiConfig = getCompleteApiConfig(modelType);
|
||||
const apiClient = getModelApiClient(modelType);
|
||||
const displayName = apiConfig?.config?.displayName ?? 'Model';
|
||||
|
||||
if (!apiConfig?.endpoints?.refreshUpdates) {
|
||||
console.warn('Refresh updates endpoint not configured for model type:', modelType);
|
||||
onComplete?.({ status: 'unsupported', records: [], error: null });
|
||||
return { status: 'unsupported', records: [], error: null };
|
||||
}
|
||||
|
||||
const loadingMessage = translate(
|
||||
'sidebar.folderUpdateCheck.loading',
|
||||
{ type: displayName },
|
||||
`Checking ${displayName} updates for this folder...`
|
||||
);
|
||||
|
||||
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
||||
state.loadingManager?.showCancelButton?.(() => apiClient.cancelTask());
|
||||
|
||||
let status = 'success';
|
||||
let records = [];
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(apiConfig.endpoints.refreshUpdates, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_path: folderPath, force: false })
|
||||
});
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
if (!response.ok || payload.success !== true) {
|
||||
if (payload?.status === 'cancelled') {
|
||||
showToast('toast.api.operationCancelled', {}, 'info');
|
||||
return { status: 'cancelled', records: [], error: null };
|
||||
}
|
||||
const errorMessage = payload?.error || response.statusText || 'Unknown error';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
records = Array.isArray(payload.records) ? payload.records : [];
|
||||
|
||||
if (records.length > 0) {
|
||||
showToast('sidebar.folderUpdateCheck.success', { count: records.length, type: displayName }, 'success');
|
||||
} else {
|
||||
showToast('sidebar.folderUpdateCheck.none', { type: displayName }, 'info');
|
||||
}
|
||||
|
||||
await resetAndReload(false);
|
||||
} catch (err) {
|
||||
status = 'error';
|
||||
error = err instanceof Error ? err : new Error(String(err));
|
||||
console.error('Error checking folder model updates:', error);
|
||||
showToast(
|
||||
'sidebar.folderUpdateCheck.error',
|
||||
{ message: error?.message ?? 'Unknown error', type: displayName },
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
state.loadingManager?.hide?.();
|
||||
if (typeof state.loadingManager?.restoreProgressBar === 'function') {
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
onComplete?.({ status, records, error });
|
||||
}
|
||||
|
||||
return { status, records, error };
|
||||
}
|
||||
|
||||
function getTypePlural(displayName) {
|
||||
if (!displayName) {
|
||||
return 'models';
|
||||
|
||||
Reference in New Issue
Block a user