From 9bebcc9a4be92e41de766de5ea4679d4be083919 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 4 Sep 2025 15:24:54 +0800 Subject: [PATCH] feat(bulk): implement bulk context menu for model operations and remove bulk operations panel --- static/css/components/menu.css | 23 +- .../components/ContextMenu/BulkContextMenu.js | 123 +++++++++ static/js/core.js | 5 + static/js/managers/BulkManager.js | 249 ++---------------- templates/components/context_menu.html | 28 ++ templates/components/controls.html | 29 -- templates/loras.html | 2 - 7 files changed, 192 insertions(+), 267 deletions(-) create mode 100644 static/js/components/ContextMenu/BulkContextMenu.js diff --git a/static/css/components/menu.css b/static/css/components/menu.css index 566f71db..481bb213 100644 --- a/static/css/components/menu.css +++ b/static/css/components/menu.css @@ -176,11 +176,6 @@ background: linear-gradient(45deg, #4a90e2, #357abd); } -/* Remove old node-color-indicator styles */ -.node-color-indicator { - display: none; -} - .send-all-item { border-top: 1px solid var(--border-color); font-weight: 500; @@ -217,4 +212,22 @@ font-size: 12px; color: var(--text-muted); 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; } \ No newline at end of file diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js new file mode 100644 index 00000000..ef206b77 --- /dev/null +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -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}`); + } + } +} diff --git a/static/js/core.js b/static/js/core.js index 1a250aca..21839d97 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -15,6 +15,7 @@ import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; import { migrateStorageItems } from './utils/storageHelpers.js'; import { i18n } from './i18n/index.js'; import { onboardingManager } from './managers/OnboardingManager.js'; +import { BulkContextMenu } from './components/ContextMenu/BulkContextMenu.js'; // Core application class export class AppCore { @@ -55,6 +56,10 @@ export class AppCore { // Initialize the bulk manager bulkManager.initialize(); + // Initialize bulk context menu + const bulkContextMenu = new BulkContextMenu(); + bulkManager.setBulkContextMenu(bulkContextMenu); + // Initialize the example images manager exampleImagesManager.initialize(); // Initialize the help manager diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 5dc2bc97..8946bc11 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -2,18 +2,14 @@ import { state, getCurrentPageState } from '../state/index.js'; import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; -import { moveManager } from './MoveManager.js'; import { getModelApiClient } from '../api/modelApiFactory.js'; import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js'; -import { updateElementText } from '../utils/i18nHelpers.js'; export class BulkManager { constructor() { this.bulkBtn = document.getElementById('bulkOperationsBtn'); - this.bulkPanel = document.getElementById('bulkOperationsPanel'); - this.isStripVisible = false; - - this.stripMaxThumbnails = 50; + // Remove bulk panel references since we're using context menu now + this.bulkContextMenu = null; // Will be set by core initialization // Model type specific action configurations this.actionConfig = { @@ -46,41 +42,13 @@ export class BulkManager { this.setupGlobalKeyboardListeners(); } + setBulkContextMenu(bulkContextMenu) { + this.bulkContextMenu = bulkContextMenu; + } + setupEventListeners() { - // Bulk operations button listeners - 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"]'); - 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()); - } + // Only setup bulk mode toggle button listener now + // Context menu actions are handled by BulkContextMenu } setupGlobalKeyboardListeners() { @@ -115,60 +83,15 @@ export class BulkManager { 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); if (!state.bulkMode) { this.clearSelection(); - // TODO: - document.querySelectorAll('.model-card').forEach(card => { - const actions = card.querySelectorAll('.card-actions, .card-button'); - 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'; + // Hide context menu when exiting bulk mode + if (this.bulkContextMenu) { + this.bulkContextMenu.hideMenu(); + } } } @@ -177,27 +100,10 @@ export class BulkManager { card.classList.remove('selected'); }); state.selectedModels.clear(); - this.updateSelectedCount(); - this.hideThumbnailStrip(); - } - - updateSelectedCount() { - const countElement = document.getElementById('selectedCount'); - 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); - } + // Update context menu header if visible + if (this.bulkContextMenu) { + this.bulkContextMenu.updateSelectedCountHeader(); } } @@ -222,10 +128,9 @@ export class BulkManager { }); } - this.updateSelectedCount(); - - if (this.isStripVisible) { - this.updateThumbnailStrip(); + // Update context menu header if visible + if (this.bulkContextMenu) { + this.bulkContextMenu.updateSelectedCountHeader(); } } @@ -277,8 +182,6 @@ export class BulkManager { card.classList.remove('selected'); } }); - - this.updateSelectedCount(); } async copyAllModelsSyntax() { @@ -413,115 +316,6 @@ export class BulkManager { showToast('toast.models.deleteFailedGeneral', {}, 'error'); } } - - 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 = ` - - ${metadata.modelName} - - `; - } else { - thumbnail.innerHTML = ` - ${metadata.modelName} - ${metadata.modelName} - - `; - } - - 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) { const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`); @@ -530,13 +324,6 @@ export class BulkManager { } state.selectedModels.delete(filepath); - - this.updateSelectedCount(); - this.updateThumbnailStrip(); - - if (state.selectedModels.size === 0) { - this.hideThumbnailStrip(); - } } selectAllVisibleModels() { diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index ff488290..b9cb211a 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -44,6 +44,34 @@ +
+
+ + {{ t('loras.bulkOperations.selected', {'count': 0}) }} +
+
+
+ {{ t('loras.bulkOperations.sendToWorkflow') }} +
+
+ {{ t('loras.bulkOperations.copyAll') }} +
+
+ {{ t('loras.bulkOperations.refreshAll') }} +
+
+ {{ t('loras.bulkOperations.moveAll') }} +
+
+
+ {{ t('loras.bulkOperations.deleteAll') }} +
+
+
+ {{ t('loras.bulkOperations.clear') }} +
+
+

{{ t('modals.contentRating.title') }}

diff --git a/templates/components/controls.html b/templates/components/controls.html index dc1e1bf5..8c9f5af5 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -98,33 +98,4 @@
-
- - - \ No newline at end of file diff --git a/templates/loras.html b/templates/loras.html index b158632f..3ece6a10 100644 --- a/templates/loras.html +++ b/templates/loras.html @@ -16,8 +16,6 @@
- - {% endblock %} {% block overlay %}