mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Merge pull request #619 from willmiao/codex/add-check-for-updates-in-bulk-context-menu
feat(bulk): add selected update check
This commit is contained in:
@@ -569,6 +569,35 @@ export class BaseModelApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshUpdatesForModels(modelIds, { force = false } = {}) {
|
||||
if (!Array.isArray(modelIds) || modelIds.length === 0) {
|
||||
throw new Error('No model IDs provided');
|
||||
}
|
||||
|
||||
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_ids: modelIds,
|
||||
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) {
|
||||
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async fetchCivitaiVersions(modelId, source = null) {
|
||||
try {
|
||||
let requestUrl = `${this.apiConfig.endpoints.civitaiVersions}/${modelId}`;
|
||||
|
||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]');
|
||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||
const checkUpdatesItem = this.menu.querySelector('[data-action="check-updates"]');
|
||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||
@@ -49,6 +50,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (refreshAllItem) {
|
||||
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||
}
|
||||
if (checkUpdatesItem) {
|
||||
checkUpdatesItem.style.display = config.checkUpdates ? 'flex' : 'none';
|
||||
}
|
||||
if (moveAllItem) {
|
||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||
}
|
||||
@@ -105,6 +109,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'refresh-all':
|
||||
bulkManager.refreshAllMetadata();
|
||||
break;
|
||||
case 'check-updates':
|
||||
bulkManager.checkUpdatesForSelectedModels();
|
||||
break;
|
||||
case 'move-all':
|
||||
window.moveManager.showMoveModal('bulk');
|
||||
break;
|
||||
|
||||
@@ -436,6 +436,12 @@ export function createModelCard(model, modelType) {
|
||||
const hasUpdateAvailable = Boolean(model.update_available);
|
||||
card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false';
|
||||
|
||||
const civitaiData = model.civitai || {};
|
||||
const modelId = civitaiData?.modelId ?? civitaiData?.model_id;
|
||||
if (modelId !== undefined && modelId !== null && modelId !== '') {
|
||||
card.dataset.modelId = modelId;
|
||||
}
|
||||
|
||||
// LoRA specific data
|
||||
if (modelType === MODEL_TYPES.LORA) {
|
||||
card.dataset.usage_tips = model.usage_tips;
|
||||
|
||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
||||
sendToWorkflow: true,
|
||||
copyAll: true,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -44,6 +45,7 @@ export class BulkManager {
|
||||
sendToWorkflow: false,
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -54,6 +56,7 @@ export class BulkManager {
|
||||
sendToWorkflow: false,
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
moveAll: false,
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
@@ -271,14 +274,9 @@ export class BulkManager {
|
||||
} else {
|
||||
card.classList.add('selected');
|
||||
state.selectedModels.add(filepath);
|
||||
|
||||
|
||||
// Cache the metadata for this model
|
||||
const metadataCache = this.getMetadataCache();
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
|
||||
// Update context menu header if visible
|
||||
@@ -290,7 +288,7 @@ export class BulkManager {
|
||||
getMetadataCache() {
|
||||
const currentType = state.currentPageType;
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
|
||||
// Initialize metadata cache if it doesn't exist
|
||||
if (currentType === MODEL_TYPES.LORA) {
|
||||
if (!state.loraMetadataCache) {
|
||||
@@ -305,6 +303,89 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
parseModelId(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
updateMetadataCacheFromCard(filepath, card) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const existing = metadataCache.get(filepath) || {};
|
||||
const modelId = this.parseModelId(card.dataset.modelId);
|
||||
|
||||
const updated = {
|
||||
...existing,
|
||||
fileName: card.dataset.file_name ?? existing.fileName,
|
||||
usageTips: card.dataset.usage_tips ?? existing.usageTips,
|
||||
modelName: card.dataset.name ?? existing.modelName,
|
||||
};
|
||||
|
||||
if (modelId !== null) {
|
||||
updated.modelId = modelId;
|
||||
}
|
||||
|
||||
metadataCache.set(filepath, updated);
|
||||
}
|
||||
|
||||
escapeAttributeValue(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
getModelIdForFilePath(filePath) {
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const cached = metadataCache.get(filePath);
|
||||
if (cached && typeof cached.modelId === 'number') {
|
||||
return cached.modelId;
|
||||
}
|
||||
|
||||
const escapedPath = this.escapeAttributeValue(filePath);
|
||||
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||
if (!card) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.updateMetadataCacheFromCard(filePath, card);
|
||||
const updated = metadataCache.get(filePath);
|
||||
return updated && typeof updated.modelId === 'number' ? updated.modelId : null;
|
||||
}
|
||||
|
||||
collectSelectedModelIds() {
|
||||
const metadataCache = this.getMetadataCache();
|
||||
const ids = [];
|
||||
let missingCount = 0;
|
||||
|
||||
for (const filepath of state.selectedModels) {
|
||||
const cached = metadataCache.get(filepath);
|
||||
let modelId = cached && typeof cached.modelId === 'number' ? cached.modelId : null;
|
||||
if (modelId === null) {
|
||||
modelId = this.getModelIdForFilePath(filepath);
|
||||
}
|
||||
|
||||
if (typeof modelId === 'number') {
|
||||
ids.push(modelId);
|
||||
} else {
|
||||
missingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueIds = Array.from(new Set(ids));
|
||||
return { ids: uniqueIds, missingCount };
|
||||
}
|
||||
|
||||
applySelectionState() {
|
||||
if (!state.bulkMode) return;
|
||||
|
||||
@@ -312,13 +393,8 @@ export class BulkManager {
|
||||
const filepath = card.dataset.filepath;
|
||||
if (state.selectedModels.has(filepath)) {
|
||||
card.classList.add('selected');
|
||||
|
||||
const metadataCache = this.getMetadataCache();
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
@@ -477,12 +553,14 @@ export class BulkManager {
|
||||
state.virtualScroller.items.forEach(item => {
|
||||
if (item && item.file_path) {
|
||||
state.selectedModels.add(item.file_path);
|
||||
|
||||
|
||||
if (!metadataCache.has(item.file_path)) {
|
||||
const modelId = this.parseModelId(item?.civitai?.modelId);
|
||||
metadataCache.set(item.file_path, {
|
||||
fileName: item.file_name,
|
||||
usageTips: item.usage_tips || '{}',
|
||||
modelName: item.name || item.file_name
|
||||
modelName: item.name || item.file_name,
|
||||
...(modelId !== null ? { modelId } : {})
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -521,12 +599,7 @@ export class BulkManager {
|
||||
if (metadata) {
|
||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||
if (card) {
|
||||
metadataCache.set(filepath, {
|
||||
...metadata,
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +614,71 @@ export class BulkManager {
|
||||
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async checkUpdatesForSelectedModels() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentType = state.currentPageType;
|
||||
const currentConfig = MODEL_CONFIG[currentType] || MODEL_CONFIG[MODEL_TYPES.LORA];
|
||||
const typeLabel = (currentConfig?.displayName || 'Model').toLowerCase();
|
||||
|
||||
const { ids: modelIds, missingCount } = this.collectSelectedModelIds();
|
||||
|
||||
if (modelIds.length === 0) {
|
||||
showToast('toast.models.bulkUpdatesMissing', { type: typeLabel }, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (missingCount > 0) {
|
||||
showToast('toast.models.bulkUpdatesPartialMissing', { missing: missingCount, type: typeLabel }, 'info');
|
||||
}
|
||||
|
||||
const apiClient = getModelApiClient();
|
||||
if (!apiClient || typeof apiClient.refreshUpdatesForModels !== 'function') {
|
||||
console.warn('Model API client does not support refreshUpdatesForModels');
|
||||
showToast('toast.models.bulkUpdatesFailed', { type: typeLabel, message: 'Operation not supported' }, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMessage = translate(
|
||||
'toast.models.bulkUpdatesChecking',
|
||||
{ count: state.selectedModels.size, type: typeLabel },
|
||||
`Checking selected ${typeLabel}(s) for updates...`
|
||||
);
|
||||
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
||||
|
||||
try {
|
||||
const response = await apiClient.refreshUpdatesForModels(modelIds);
|
||||
const records = Array.isArray(response?.records) ? response.records : [];
|
||||
const updatesCount = records.length;
|
||||
|
||||
if (updatesCount > 0) {
|
||||
showToast('toast.models.bulkUpdatesSuccess', { count: updatesCount, type: typeLabel }, 'success');
|
||||
} else {
|
||||
showToast('toast.models.bulkUpdatesNone', { type: typeLabel }, 'info');
|
||||
}
|
||||
|
||||
await resetAndReload(false);
|
||||
} catch (error) {
|
||||
console.error('Error checking updates for selected models:', error);
|
||||
showToast(
|
||||
'toast.models.bulkUpdatesFailed',
|
||||
{ type: typeLabel, message: error?.message ?? 'Unknown error' },
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
if (state.loadingManager?.hide) {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
if (typeof state.loadingManager?.restoreProgressBar === 'function') {
|
||||
state.loadingManager.restoreProgressBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showBulkAddTagsModal() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
@@ -1263,15 +1400,11 @@ export class BulkManager {
|
||||
// Add to selection if intersecting
|
||||
newSelection.add(filepath);
|
||||
card.classList.add('selected');
|
||||
|
||||
|
||||
// Cache metadata if not already cached
|
||||
const metadataCache = this.getMetadataCache();
|
||||
if (!metadataCache.has(filepath)) {
|
||||
metadataCache.set(filepath, {
|
||||
fileName: card.dataset.file_name,
|
||||
usageTips: card.dataset.usage_tips,
|
||||
modelName: card.dataset.name
|
||||
});
|
||||
this.updateMetadataCacheFromCard(filepath, card);
|
||||
}
|
||||
} else if (!this.initialSelectedModels.has(filepath)) {
|
||||
// Remove from selection if not intersecting and wasn't initially selected
|
||||
|
||||
Reference in New Issue
Block a user