mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat(bulk): implement bulk context menu for model operations and remove bulk operations panel
This commit is contained in:
@@ -176,11 +176,6 @@
|
|||||||
background: linear-gradient(45deg, #4a90e2, #357abd);
|
background: linear-gradient(45deg, #4a90e2, #357abd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove old node-color-indicator styles */
|
|
||||||
.node-color-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-all-item {
|
.send-all-item {
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -218,3 +213,21 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bulk Context Menu Header */
|
||||||
|
.bulk-context-header {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: var(--border-radius-xs) var(--border-radius-xs) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-context-header i {
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
123
static/js/components/ContextMenu/BulkContextMenu.js
Normal file
123
static/js/components/ContextMenu/BulkContextMenu.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
|
import { state } from '../../state/index.js';
|
||||||
|
import { bulkManager } from '../../managers/BulkManager.js';
|
||||||
|
import { translate, updateElementText } from '../../utils/i18nHelpers.js';
|
||||||
|
import { MODEL_TYPES } from '../../api/apiConfig.js';
|
||||||
|
|
||||||
|
export class BulkContextMenu extends BaseContextMenu {
|
||||||
|
constructor() {
|
||||||
|
super('bulkContextMenu', '.model-card.selected');
|
||||||
|
this.setupBulkMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Override parent init to handle bulk-specific context menu logic
|
||||||
|
document.addEventListener('click', () => this.hideMenu());
|
||||||
|
|
||||||
|
document.addEventListener('contextmenu', (e) => {
|
||||||
|
const card = e.target.closest('.model-card');
|
||||||
|
if (!card || !state.bulkMode) {
|
||||||
|
this.hideMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show bulk menu only if right-clicking on a selected card
|
||||||
|
if (card.classList.contains('selected')) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.showMenu(e.clientX, e.clientY, card);
|
||||||
|
} else {
|
||||||
|
this.hideMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle menu item clicks
|
||||||
|
this.menu.addEventListener('click', (e) => {
|
||||||
|
const menuItem = e.target.closest('.context-menu-item');
|
||||||
|
if (!menuItem || !this.currentCard) return;
|
||||||
|
|
||||||
|
const action = menuItem.dataset.action;
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
this.handleMenuAction(action, menuItem);
|
||||||
|
this.hideMenu();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupBulkMenuItems() {
|
||||||
|
if (!this.menu) return;
|
||||||
|
|
||||||
|
// Update menu items visibility based on current model type
|
||||||
|
this.updateMenuItemsForModelType();
|
||||||
|
|
||||||
|
// Update selected count in header
|
||||||
|
this.updateSelectedCountHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMenuItemsForModelType() {
|
||||||
|
const currentModelType = state.currentPageType;
|
||||||
|
const config = bulkManager.actionConfig[currentModelType];
|
||||||
|
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
// Update button visibility based on model type
|
||||||
|
const sendToWorkflowItem = this.menu.querySelector('[data-action="send-to-workflow"]');
|
||||||
|
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||||
|
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||||
|
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||||
|
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||||
|
|
||||||
|
if (sendToWorkflowItem) {
|
||||||
|
sendToWorkflowItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (copyAllItem) {
|
||||||
|
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (refreshAllItem) {
|
||||||
|
refreshAllItem.style.display = config.refreshAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (moveAllItem) {
|
||||||
|
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
if (deleteAllItem) {
|
||||||
|
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedCountHeader() {
|
||||||
|
const headerElement = this.menu.querySelector('.bulk-context-header');
|
||||||
|
if (headerElement) {
|
||||||
|
updateElementText(headerElement, 'loras.bulkOperations.selected', { count: state.selectedModels.size });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenu(x, y, card) {
|
||||||
|
this.updateMenuItemsForModelType();
|
||||||
|
this.updateSelectedCountHeader();
|
||||||
|
super.showMenu(x, y, card);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMenuAction(action, menuItem) {
|
||||||
|
switch (action) {
|
||||||
|
case 'send-to-workflow':
|
||||||
|
bulkManager.sendAllModelsToWorkflow();
|
||||||
|
break;
|
||||||
|
case 'copy-all':
|
||||||
|
bulkManager.copyAllModelsSyntax();
|
||||||
|
break;
|
||||||
|
case 'refresh-all':
|
||||||
|
bulkManager.refreshAllMetadata();
|
||||||
|
break;
|
||||||
|
case 'move-all':
|
||||||
|
window.moveManager.showMoveModal('bulk');
|
||||||
|
break;
|
||||||
|
case 'delete-all':
|
||||||
|
bulkManager.showBulkDeleteModal();
|
||||||
|
break;
|
||||||
|
case 'clear':
|
||||||
|
bulkManager.clearSelection();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown bulk action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
|||||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||||
import { i18n } from './i18n/index.js';
|
import { i18n } from './i18n/index.js';
|
||||||
import { onboardingManager } from './managers/OnboardingManager.js';
|
import { onboardingManager } from './managers/OnboardingManager.js';
|
||||||
|
import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js';
|
||||||
|
|
||||||
// Core application class
|
// Core application class
|
||||||
export class AppCore {
|
export class AppCore {
|
||||||
@@ -55,6 +56,10 @@ export class AppCore {
|
|||||||
// Initialize the bulk manager
|
// Initialize the bulk manager
|
||||||
bulkManager.initialize();
|
bulkManager.initialize();
|
||||||
|
|
||||||
|
// Initialize bulk context menu
|
||||||
|
const bulkContextMenu = new BulkContextMenu();
|
||||||
|
bulkManager.setBulkContextMenu(bulkContextMenu);
|
||||||
|
|
||||||
// Initialize the example images manager
|
// Initialize the example images manager
|
||||||
exampleImagesManager.initialize();
|
exampleImagesManager.initialize();
|
||||||
// Initialize the help manager
|
// Initialize the help manager
|
||||||
|
|||||||
@@ -2,18 +2,14 @@ import { state, getCurrentPageState } from '../state/index.js';
|
|||||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { moveManager } from './MoveManager.js';
|
|
||||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||||
import { updateElementText } from '../utils/i18nHelpers.js';
|
|
||||||
|
|
||||||
export class BulkManager {
|
export class BulkManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
||||||
this.bulkPanel = document.getElementById('bulkOperationsPanel');
|
// Remove bulk panel references since we're using context menu now
|
||||||
this.isStripVisible = false;
|
this.bulkContextMenu = null; // Will be set by core initialization
|
||||||
|
|
||||||
this.stripMaxThumbnails = 50;
|
|
||||||
|
|
||||||
// Model type specific action configurations
|
// Model type specific action configurations
|
||||||
this.actionConfig = {
|
this.actionConfig = {
|
||||||
@@ -46,41 +42,13 @@ export class BulkManager {
|
|||||||
this.setupGlobalKeyboardListeners();
|
this.setupGlobalKeyboardListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBulkContextMenu(bulkContextMenu) {
|
||||||
|
this.bulkContextMenu = bulkContextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Bulk operations button listeners
|
// Only setup bulk mode toggle button listener now
|
||||||
const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]');
|
// Context menu actions are handled by BulkContextMenu
|
||||||
const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]');
|
|
||||||
const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]');
|
|
||||||
const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]');
|
|
||||||
const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]');
|
|
||||||
const clearBtn = this.bulkPanel?.querySelector('[data-action="clear"]');
|
|
||||||
|
|
||||||
if (sendToWorkflowBtn) {
|
|
||||||
sendToWorkflowBtn.addEventListener('click', () => this.sendAllModelsToWorkflow());
|
|
||||||
}
|
|
||||||
if (copyAllBtn) {
|
|
||||||
copyAllBtn.addEventListener('click', () => this.copyAllModelsSyntax());
|
|
||||||
}
|
|
||||||
if (refreshAllBtn) {
|
|
||||||
refreshAllBtn.addEventListener('click', () => this.refreshAllMetadata());
|
|
||||||
}
|
|
||||||
if (moveAllBtn) {
|
|
||||||
moveAllBtn.addEventListener('click', () => {
|
|
||||||
moveManager.showMoveModal('bulk');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (deleteAllBtn) {
|
|
||||||
deleteAllBtn.addEventListener('click', () => this.showBulkDeleteModal());
|
|
||||||
}
|
|
||||||
if (clearBtn) {
|
|
||||||
clearBtn.addEventListener('click', () => this.clearSelection());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected count click listener
|
|
||||||
const selectedCount = document.getElementById('selectedCount');
|
|
||||||
if (selectedCount) {
|
|
||||||
selectedCount.addEventListener('click', () => this.toggleThumbnailStrip());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupGlobalKeyboardListeners() {
|
setupGlobalKeyboardListeners() {
|
||||||
@@ -115,60 +83,15 @@ export class BulkManager {
|
|||||||
|
|
||||||
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
||||||
|
|
||||||
if (state.bulkMode) {
|
|
||||||
this.bulkPanel.classList.remove('hidden');
|
|
||||||
this.updateActionButtonsVisibility();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.bulkPanel.classList.add('visible');
|
|
||||||
}, 10);
|
|
||||||
} else {
|
|
||||||
this.bulkPanel.classList.remove('visible');
|
|
||||||
setTimeout(() => {
|
|
||||||
this.bulkPanel.classList.add('hidden');
|
|
||||||
}, 400);
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
if (!state.bulkMode) {
|
if (!state.bulkMode) {
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
|
|
||||||
// TODO:
|
// Hide context menu when exiting bulk mode
|
||||||
document.querySelectorAll('.model-card').forEach(card => {
|
if (this.bulkContextMenu) {
|
||||||
const actions = card.querySelectorAll('.card-actions, .card-button');
|
this.bulkContextMenu.hideMenu();
|
||||||
actions.forEach(action => action.style.display = 'flex');
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionButtonsVisibility() {
|
|
||||||
const currentModelType = state.currentPageType;
|
|
||||||
const config = this.actionConfig[currentModelType];
|
|
||||||
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
// Update button visibility based on model type
|
|
||||||
const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]');
|
|
||||||
const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]');
|
|
||||||
const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]');
|
|
||||||
const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]');
|
|
||||||
const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]');
|
|
||||||
|
|
||||||
if (sendToWorkflowBtn) {
|
|
||||||
sendToWorkflowBtn.style.display = config.sendToWorkflow ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (copyAllBtn) {
|
|
||||||
copyAllBtn.style.display = config.copyAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (refreshAllBtn) {
|
|
||||||
refreshAllBtn.style.display = config.refreshAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (moveAllBtn) {
|
|
||||||
moveAllBtn.style.display = config.moveAll ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
if (deleteAllBtn) {
|
|
||||||
deleteAllBtn.style.display = config.deleteAll ? 'block' : 'none';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,27 +100,10 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
});
|
});
|
||||||
state.selectedModels.clear();
|
state.selectedModels.clear();
|
||||||
this.updateSelectedCount();
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectedCount() {
|
// Update context menu header if visible
|
||||||
const countElement = document.getElementById('selectedCount');
|
if (this.bulkContextMenu) {
|
||||||
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
if (countElement) {
|
|
||||||
// Use i18nHelpers.js to update the count text
|
|
||||||
updateElementText(countElement, 'loras.bulkOperations.selected', { count: state.selectedModels.size });
|
|
||||||
|
|
||||||
const existingCaret = countElement.querySelector('.dropdown-caret');
|
|
||||||
if (existingCaret) {
|
|
||||||
existingCaret.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`;
|
|
||||||
existingCaret.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden';
|
|
||||||
} else {
|
|
||||||
const caretIcon = document.createElement('i');
|
|
||||||
caretIcon.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`;
|
|
||||||
caretIcon.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden';
|
|
||||||
countElement.appendChild(caretIcon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,10 +128,9 @@ export class BulkManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateSelectedCount();
|
// Update context menu header if visible
|
||||||
|
if (this.bulkContextMenu) {
|
||||||
if (this.isStripVisible) {
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
this.updateThumbnailStrip();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,8 +182,6 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateSelectedCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyAllModelsSyntax() {
|
async copyAllModelsSyntax() {
|
||||||
@@ -414,115 +317,6 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleThumbnailStrip() {
|
|
||||||
if (state.selectedModels.size === 0) return;
|
|
||||||
|
|
||||||
const existing = document.querySelector('.selected-thumbnails-strip');
|
|
||||||
if (existing) {
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
} else {
|
|
||||||
this.showThumbnailStrip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showThumbnailStrip() {
|
|
||||||
const strip = document.createElement('div');
|
|
||||||
strip.className = 'selected-thumbnails-strip';
|
|
||||||
|
|
||||||
const thumbnailContainer = document.createElement('div');
|
|
||||||
thumbnailContainer.className = 'thumbnails-container';
|
|
||||||
strip.appendChild(thumbnailContainer);
|
|
||||||
|
|
||||||
this.bulkPanel.parentNode.insertBefore(strip, this.bulkPanel);
|
|
||||||
|
|
||||||
this.updateThumbnailStrip();
|
|
||||||
|
|
||||||
this.isStripVisible = true;
|
|
||||||
this.updateSelectedCount();
|
|
||||||
|
|
||||||
setTimeout(() => strip.classList.add('visible'), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideThumbnailStrip() {
|
|
||||||
const strip = document.querySelector('.selected-thumbnails-strip');
|
|
||||||
if (strip && this.isStripVisible) {
|
|
||||||
strip.classList.remove('visible');
|
|
||||||
|
|
||||||
this.isStripVisible = false;
|
|
||||||
|
|
||||||
const countElement = document.getElementById('selectedCount');
|
|
||||||
if (countElement) {
|
|
||||||
const caret = countElement.querySelector('.dropdown-caret');
|
|
||||||
if (caret) {
|
|
||||||
caret.className = 'fas fa-caret-up dropdown-caret';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (strip.parentNode) {
|
|
||||||
strip.parentNode.removeChild(strip);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateThumbnailStrip() {
|
|
||||||
const container = document.querySelector('.thumbnails-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
const selectedModels = Array.from(state.selectedModels);
|
|
||||||
|
|
||||||
if (selectedModels.length > this.stripMaxThumbnails) {
|
|
||||||
const counter = document.createElement('div');
|
|
||||||
counter.className = 'strip-counter';
|
|
||||||
counter.textContent = `Showing ${this.stripMaxThumbnails} of ${selectedModels.length} selected`;
|
|
||||||
container.appendChild(counter);
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailsToShow = selectedModels.slice(0, this.stripMaxThumbnails);
|
|
||||||
const metadataCache = this.getMetadataCache();
|
|
||||||
|
|
||||||
thumbnailsToShow.forEach(filepath => {
|
|
||||||
const metadata = metadataCache.get(filepath);
|
|
||||||
if (!metadata) return;
|
|
||||||
|
|
||||||
const thumbnail = document.createElement('div');
|
|
||||||
thumbnail.className = 'selected-thumbnail';
|
|
||||||
thumbnail.dataset.filepath = filepath;
|
|
||||||
|
|
||||||
if (metadata.isVideo) {
|
|
||||||
thumbnail.innerHTML = `
|
|
||||||
<video autoplay loop muted playsinline>
|
|
||||||
<source src="${metadata.previewUrl}" type="video/mp4">
|
|
||||||
</video>
|
|
||||||
<span class="thumbnail-name" title="${metadata.modelName}">${metadata.modelName}</span>
|
|
||||||
<button class="thumbnail-remove"><i class="fas fa-times"></i></button>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
thumbnail.innerHTML = `
|
|
||||||
<img src="${metadata.previewUrl}" alt="${metadata.modelName}">
|
|
||||||
<span class="thumbnail-name" title="${metadata.modelName}">${metadata.modelName}</span>
|
|
||||||
<button class="thumbnail-remove"><i class="fas fa-times"></i></button>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbnail.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('.thumbnail-remove')) {
|
|
||||||
this.deselectItem(filepath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
thumbnail.querySelector('.thumbnail-remove').addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.deselectItem(filepath);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(thumbnail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectItem(filepath) {
|
deselectItem(filepath) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
@@ -530,13 +324,6 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.selectedModels.delete(filepath);
|
state.selectedModels.delete(filepath);
|
||||||
|
|
||||||
this.updateSelectedCount();
|
|
||||||
this.updateThumbnailStrip();
|
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
|
||||||
this.hideThumbnailStrip();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAllVisibleModels() {
|
selectAllVisibleModels() {
|
||||||
|
|||||||
@@ -44,6 +44,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="bulkContextMenu" class="context-menu">
|
||||||
|
<div class="bulk-context-header">
|
||||||
|
<i class="fas fa-th-large"></i>
|
||||||
|
<span>{{ t('loras.bulkOperations.selected', {'count': 0}) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item" data-action="send-to-workflow">
|
||||||
|
<i class="fas fa-paper-plane"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="copy-all">
|
||||||
|
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="refresh-all">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="move-all">
|
||||||
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item delete-item" data-action="delete-all">
|
||||||
|
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<div class="context-menu-item" data-action="clear">
|
||||||
|
<i class="fas fa-times"></i> <span>{{ t('loras.bulkOperations.clear') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
<div id="nsfwLevelSelector" class="nsfw-level-selector">
|
||||||
<div class="nsfw-level-header">
|
<div class="nsfw-level-header">
|
||||||
<h3>{{ t('modals.contentRating.title') }}</h3>
|
<h3>{{ t('modals.contentRating.title') }}</h3>
|
||||||
|
|||||||
@@ -99,32 +99,3 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add bulk operations panel (initially hidden) -->
|
|
||||||
<div id="bulkOperationsPanel" class="bulk-operations-panel hidden">
|
|
||||||
<div class="bulk-operations-header">
|
|
||||||
<span id="selectedCount" class="selectable-count" title="{{ t('loras.bulkOperations.viewSelected') }}">
|
|
||||||
0 {{ t('loras.bulkOperations.selectedSuffix') }} <i class="fas fa-caret-down dropdown-caret"></i>
|
|
||||||
</span>
|
|
||||||
<div class="bulk-operations-actions">
|
|
||||||
<button data-action="send-to-workflow" title="{{ t('loras.bulkOperations.sendToWorkflow') }}">
|
|
||||||
<i class="fas fa-arrow-right"></i> <span>{{ t('loras.bulkOperations.sendToWorkflow') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="copy-all" title="{{ t('loras.bulkOperations.copyAll') }}">
|
|
||||||
<i class="fas fa-copy"></i> <span>{{ t('loras.bulkOperations.copyAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="refresh-all" title="{{ t('loras.bulkOperations.refreshAll') }}">
|
|
||||||
<i class="fas fa-sync-alt"></i> <span>{{ t('loras.bulkOperations.refreshAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="move-all" title="{{ t('loras.bulkOperations.moveAll') }}">
|
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="delete-all" title="{{ t('loras.bulkOperations.deleteAll') }}" class="danger-btn">
|
|
||||||
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
|
||||||
</button>
|
|
||||||
<button data-action="clear" title="{{ t('loras.bulkOperations.clear') }}">
|
|
||||||
<i class="fas fa-times"></i> <span>{{ t('loras.bulkOperations.clear') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -16,8 +16,6 @@
|
|||||||
<div class="card-grid" id="modelGrid">
|
<div class="card-grid" id="modelGrid">
|
||||||
<!-- Cards will be dynamically inserted here -->
|
<!-- Cards will be dynamically inserted here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block overlay %}
|
{% block overlay %}
|
||||||
|
|||||||
Reference in New Issue
Block a user