mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-07 00:46:44 -03:00
feat(bulk): add bulk favorite/unfavorite toggle with context-sensitive single menu item
Replaces two separate menu items with a single smart item that dynamically switches between 'Set as Favorite' and 'Remove from Favorites' based on whether all selected items are already favorited. Shows a count badge '(3/5)' when only some items are favorited in a mixed selection. Supports all model types (LoRA, Checkpoint, Embedding) and recipes via existing per-item save/update API — no backend changes needed.
This commit is contained in:
@@ -74,6 +74,34 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (setContentRatingItem) {
|
||||
setContentRatingItem.style.display = config.setContentRating ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
const setFavoriteItem = this.menu.querySelector('[data-action="set-favorite"]');
|
||||
|
||||
if (setFavoriteItem && config.setFavorite) {
|
||||
setFavoriteItem.style.display = 'flex';
|
||||
|
||||
const total = state.selectedModels.size;
|
||||
const favoritedCount = this.countFavoritedInSelection();
|
||||
const allFavorited = total > 0 && favoritedCount === total;
|
||||
|
||||
const icon = setFavoriteItem.querySelector('i');
|
||||
const label = setFavoriteItem.querySelector('span');
|
||||
|
||||
if (allFavorited) {
|
||||
if (icon) { icon.className = 'far fa-star'; }
|
||||
if (label) { label.textContent = translate('loras.bulkOperations.unfavorite'); }
|
||||
} else {
|
||||
if (icon) { icon.className = 'fas fa-star'; }
|
||||
if (label) {
|
||||
label.textContent = favoritedCount > 0
|
||||
? translate('loras.bulkOperations.setFavoriteCount', { favorited: favoritedCount, total })
|
||||
: translate('loras.bulkOperations.setFavorite');
|
||||
}
|
||||
}
|
||||
} else if (setFavoriteItem) {
|
||||
setFavoriteItem.style.display = 'none';
|
||||
}
|
||||
|
||||
if (downloadMissingLorasItem) {
|
||||
// Only show for recipes page
|
||||
downloadMissingLorasItem.style.display = currentModelType === 'recipes' ? 'flex' : 'none';
|
||||
@@ -138,6 +166,20 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
return count;
|
||||
}
|
||||
|
||||
countFavoritedInSelection() {
|
||||
let count = 0;
|
||||
for (const filePath of state.selectedModels) {
|
||||
const escapedPath = window.CSS && typeof window.CSS.escape === 'function'
|
||||
? window.CSS.escape(filePath)
|
||||
: filePath.replace(/["\\]/g, '\\$&');
|
||||
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||
if (card && card.dataset.favorite === 'true') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
showMenu(x, y, card) {
|
||||
this.updateMenuItemsForModelType();
|
||||
this.updateSelectedCountHeader();
|
||||
@@ -185,6 +227,11 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'delete-all':
|
||||
bulkManager.showBulkDeleteModal();
|
||||
break;
|
||||
case 'set-favorite': {
|
||||
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
|
||||
bulkManager.setBulkFavorites(!allFavorited);
|
||||
break;
|
||||
}
|
||||
case 'download-missing-loras':
|
||||
this.handleDownloadMissingLoras();
|
||||
break;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { RecipeSidebarApiClient } from '../api/recipeApi.js';
|
||||
import { RecipeSidebarApiClient, updateRecipeMetadata } from '../api/recipeApi.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||
@@ -41,7 +41,9 @@ export class BulkManager {
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
setContentRating: true,
|
||||
skipMetadataRefresh: true
|
||||
skipMetadataRefresh: true,
|
||||
setFavorite: true,
|
||||
unfavorite: true
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
addTags: true,
|
||||
@@ -53,7 +55,9 @@ export class BulkManager {
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
setContentRating: false,
|
||||
skipMetadataRefresh: true
|
||||
skipMetadataRefresh: true,
|
||||
setFavorite: true,
|
||||
unfavorite: true
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
addTags: true,
|
||||
@@ -65,7 +69,9 @@ export class BulkManager {
|
||||
autoOrganize: true,
|
||||
deleteAll: true,
|
||||
setContentRating: true,
|
||||
skipMetadataRefresh: true
|
||||
skipMetadataRefresh: true,
|
||||
setFavorite: true,
|
||||
unfavorite: true
|
||||
},
|
||||
recipes: {
|
||||
addTags: false,
|
||||
@@ -77,7 +83,9 @@ export class BulkManager {
|
||||
autoOrganize: false,
|
||||
deleteAll: true,
|
||||
setContentRating: false,
|
||||
skipMetadataRefresh: false
|
||||
skipMetadataRefresh: false,
|
||||
setFavorite: true,
|
||||
unfavorite: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1090,6 +1098,60 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
async setBulkFavorites(value) {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const totalCount = state.selectedModels.size;
|
||||
const isRecipesPage = state.currentPageType === 'recipes';
|
||||
|
||||
state.loadingManager.showSimpleLoading(
|
||||
translate(value ? 'toast.models.bulkFavoriteUpdating' : 'toast.models.bulkUnfavoriteUpdating', { count: totalCount })
|
||||
);
|
||||
let cancelled = false;
|
||||
state.loadingManager.showCancelButton(() => {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
try {
|
||||
for (const filePath of state.selectedModels) {
|
||||
if (cancelled) {
|
||||
showToast('toast.api.operationCancelled', {}, 'info');
|
||||
break;
|
||||
}
|
||||
try {
|
||||
if (isRecipesPage) {
|
||||
await updateRecipeMetadata(filePath, { favorite: value });
|
||||
} else {
|
||||
const apiClient = getModelApiClient();
|
||||
await apiClient.saveModelMetadata(filePath, { favorite: value });
|
||||
}
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failureCount++;
|
||||
console.error(`Failed to set favorite=${value} for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
state.loadingManager?.hide?.();
|
||||
}
|
||||
|
||||
if (successCount === totalCount) {
|
||||
const toastKey = value ? 'modelCard.favorites.added' : 'modelCard.favorites.removed';
|
||||
showToast(toastKey, {}, 'success');
|
||||
} else if (successCount > 0) {
|
||||
const toastKey = value ? 'toast.models.bulkFavoritePartialAdded' : 'toast.models.bulkFavoritePartialRemoved';
|
||||
showToast(toastKey, { success: successCount, failed: failureCount }, 'warning');
|
||||
} else {
|
||||
showToast('toast.models.bulkFavoriteFailed', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show bulk base model modal
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user