mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-09 12:39:23 -03:00
feat(update): add per-folder update check via sidebar context menu (#944)
This commit is contained in:
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "Keine Ordner gefunden",
|
||||
"dragHint": "Elemente hierher ziehen, um Ordner zu erstellen"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Auf Updates in diesem Ordner prüfen",
|
||||
"loading": "Prüfe {type}-Updates in diesem Ordner...",
|
||||
"success": "{count} Update(s) für {type}s in diesem Ordner gefunden",
|
||||
"none": "Alle {type}s in diesem Ordner sind aktuell",
|
||||
"error": "Fehler beim Prüfen des Ordners auf {type}-Updates: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "No folders found",
|
||||
"dragHint": "Drag items here to create folders"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Check for updates in this folder",
|
||||
"loading": "Checking {type} updates for this folder...",
|
||||
"success": "Found {count} update(s) for {type}s in this folder",
|
||||
"none": "All {type}s in this folder are up to date",
|
||||
"error": "Failed to check folder for {type} updates: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "No se encontraron carpetas",
|
||||
"dragHint": "Arrastra elementos aquí para crear carpetas"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Buscar actualizaciones en esta carpeta",
|
||||
"loading": "Buscando actualizaciones de {type} en esta carpeta...",
|
||||
"success": "Se encontraron {count} actualización(es) para {type}s en esta carpeta",
|
||||
"none": "Todos los {type}s en esta carpeta están actualizados",
|
||||
"error": "Error al buscar actualizaciones de {type} en la carpeta: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "Aucun dossier trouvé",
|
||||
"dragHint": "Faites glisser des éléments ici pour créer des dossiers"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Vérifier les mises à jour dans ce dossier",
|
||||
"loading": "Vérification des mises à jour {type} dans ce dossier...",
|
||||
"success": "{count} mise(s) à jour trouvée(s) pour les {type}s dans ce dossier",
|
||||
"none": "Tous les {type}s dans ce dossier sont à jour",
|
||||
"error": "Échec de la vérification des mises à jour {type} dans ce dossier : {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "לא נמצאו תיקיות",
|
||||
"dragHint": "גרור פריטים לכאן כדי ליצור תיקיות"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "בדוק עדכונים בתיקייה זו",
|
||||
"loading": "בודק עדכוני {type} בתיקייה זו...",
|
||||
"success": "נמצאו {count} עדכון/ים עבור {type}s בתיקייה זו",
|
||||
"none": "כל ה-{type}s בתיקייה זו מעודכנים",
|
||||
"error": "נכשל בבדיקת עדכוני {type} בתיקייה: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "フォルダが見つかりません",
|
||||
"dragHint": "ここへアイテムをドラッグしてフォルダを作成します"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "このフォルダのアップデートを確認",
|
||||
"loading": "このフォルダの{type}アップデートを確認中...",
|
||||
"success": "このフォルダの{type}sに{count}件のアップデートが見つかりました",
|
||||
"none": "このフォルダのすべての{type}sは最新です",
|
||||
"error": "フォルダの{type}アップデート確認に失敗しました: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "폴더를 찾을 수 없습니다",
|
||||
"dragHint": "항목을 여기로 드래그하여 폴더를 만듭니다"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "이 폴더의 업데이트 확인",
|
||||
"loading": "이 폴더의 {type} 업데이트를 확인하는 중...",
|
||||
"success": "이 폴더에서 {type}s에 대한 {count}개 업데이트를 찾았습니다",
|
||||
"none": "이 폴더의 모든 {type}s가 최신 상태입니다",
|
||||
"error": "폴더의 {type} 업데이트 확인 실패: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "Папки не найдены",
|
||||
"dragHint": "Перетащите элементы сюда, чтобы создать папки"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "Проверить обновления в этой папке",
|
||||
"loading": "Проверка обновлений {type} в этой папке...",
|
||||
"success": "Найдено {count} обновление(й) для {type}s в этой папке",
|
||||
"none": "Все {type}s в этой папке актуальны",
|
||||
"error": "Не удалось проверить папку на наличие обновлений {type}: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "未找到文件夹",
|
||||
"dragHint": "拖拽项目到此处以创建文件夹"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "检查此文件夹的更新",
|
||||
"loading": "正在检查此文件夹中的{type}更新...",
|
||||
"success": "在此文件夹中找到 {count} 个{type}更新",
|
||||
"none": "此文件夹中的所有{type}都是最新版本",
|
||||
"error": "检查文件夹{type}更新失败: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -963,6 +963,13 @@
|
||||
"empty": {
|
||||
"noFolders": "未找到資料夾",
|
||||
"dragHint": "將項目拖到此處以建立資料夾"
|
||||
},
|
||||
"folderUpdateCheck": {
|
||||
"label": "檢查此資料夾的更新",
|
||||
"loading": "正在檢查此資料夾中的{type}更新...",
|
||||
"success": "在此資料夾中找到 {count} 個{type}更新",
|
||||
"none": "此資料夾中的所有{type}都是最新版本",
|
||||
"error": "檢查資料夾{type}更新失敗: {message}"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
|
||||
@@ -1960,6 +1960,10 @@ class ModelUpdateHandler:
|
||||
if target_model_ids:
|
||||
target_model_ids = sorted(set(target_model_ids))
|
||||
|
||||
folder_path: Optional[str] = payload.get("folder_path")
|
||||
if folder_path is not None and not isinstance(folder_path, str):
|
||||
folder_path = None
|
||||
|
||||
provider = await self._get_civitai_provider()
|
||||
if provider is None:
|
||||
return web.json_response(
|
||||
@@ -1974,6 +1978,7 @@ class ModelUpdateHandler:
|
||||
provider,
|
||||
force_refresh=force_refresh,
|
||||
target_model_ids=target_model_ids or None,
|
||||
folder_path=folder_path,
|
||||
)
|
||||
if self._service.scanner.is_cancelled():
|
||||
return web.json_response(
|
||||
|
||||
@@ -689,6 +689,7 @@ class ModelUpdateService:
|
||||
*,
|
||||
force_refresh: bool = False,
|
||||
target_model_ids: Optional[Sequence[int]] = None,
|
||||
folder_path: Optional[str] = None,
|
||||
) -> Dict[int, ModelUpdateRecord]:
|
||||
"""Refresh update information for every model present in the cache."""
|
||||
scanner.reset_cancellation()
|
||||
@@ -703,6 +704,7 @@ class ModelUpdateService:
|
||||
local_versions = await self._collect_local_versions(
|
||||
scanner,
|
||||
target_model_ids=target_filter,
|
||||
folder_path=folder_path,
|
||||
)
|
||||
total_models = len(local_versions)
|
||||
if total_models == 0:
|
||||
@@ -1276,6 +1278,7 @@ class ModelUpdateService:
|
||||
scanner,
|
||||
*,
|
||||
target_model_ids: Optional[Sequence[int]] = None,
|
||||
folder_path: Optional[str] = None,
|
||||
) -> Dict[int, List[int]]:
|
||||
cache = await scanner.get_cached_data()
|
||||
mapping: Dict[int, set[int]] = {}
|
||||
@@ -1288,7 +1291,19 @@ class ModelUpdateService:
|
||||
if not target_set:
|
||||
return {}
|
||||
|
||||
normalized_folder = None
|
||||
if folder_path is not None:
|
||||
normalized_folder = folder_path.replace("\\", "/").strip("/")
|
||||
|
||||
for item in cache.raw_data:
|
||||
# Apply folder filter first (cheapest check)
|
||||
if normalized_folder is not None:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
item_folder = (item.get("folder") or "").replace("\\", "/").strip("/")
|
||||
if item_folder != normalized_folder and not item_folder.startswith(normalized_folder + "/"):
|
||||
continue
|
||||
|
||||
civitai = item.get("civitai") if isinstance(item, dict) else None
|
||||
if not isinstance(civitai, dict):
|
||||
continue
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -150,6 +150,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Folder Context Menu -->
|
||||
<div id="sidebarFolderContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="check-folder-updates">
|
||||
<i class="fas fa-bell"></i> <span>{{ t('sidebar.folderUpdateCheck.label') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
||||
<div class="nsfw-level-header">
|
||||
<h3>{{ t('modals.contentRating.title') }}</h3>
|
||||
|
||||
Reference in New Issue
Block a user