diff --git a/app/static/js/bulk-select.js b/app/static/js/bulk-select.js index 70b70e9..c3dcc99 100644 --- a/app/static/js/bulk-select.js +++ b/app/static/js/bulk-select.js @@ -7,7 +7,8 @@ const state = { isSelectMode: false, - selectedIds: new Set() + selectedIds: new Set(), + selectionOrder: [] // Array to track order of selection }; // DOM elements (populated on init) @@ -28,6 +29,8 @@ dom.tagInput = document.getElementById('bulkTagInput'); dom.autocomplete = document.getElementById('bulkTagAutocomplete'); dom.commonTags = document.getElementById('commonTagsList'); + dom.floatingPreview = document.getElementById('floatingPreview'); + dom.floatingPreviewImage = document.getElementById('floatingPreviewImage'); if (!dom.selectBtn || !dom.panel) return; @@ -63,6 +66,30 @@ } } }); + + // Preview on hover in select mode + document.addEventListener('mouseenter', handlePreviewShow, true); + document.addEventListener('mouseleave', handlePreviewHide, true); + } + + function handlePreviewShow(e) { + if (!state.isSelectMode || !dom.floatingPreview) return; + const item = e.target.closest('.img-clickable'); + if (!item) return; + + const fullSrc = item.dataset.full; + if (fullSrc && dom.floatingPreviewImage) { + dom.floatingPreviewImage.src = fullSrc; + dom.floatingPreview.classList.add('active'); + } + } + + function handlePreviewHide(e) { + if (!dom.floatingPreview) return; + const item = e.target.closest('.img-clickable'); + if (!item) return; + + dom.floatingPreview.classList.remove('active'); } function toggleSelectMode() { @@ -104,12 +131,18 @@ if (state.selectedIds.has(id)) { state.selectedIds.delete(id); + state.selectionOrder = state.selectionOrder.filter(x => x !== id); item.classList.remove('selected'); + removeOrderBadge(item); } else { state.selectedIds.add(id); + state.selectionOrder.push(id); item.classList.add('selected'); + addOrderBadge(item, state.selectionOrder.length); } + // Update all badges to reflect current order + updateAllOrderBadges(); updateCount(); loadCommonTags(); dispatchSelectionChanged(); @@ -117,8 +150,10 @@ function clearSelection() { state.selectedIds.clear(); + state.selectionOrder = []; document.querySelectorAll('.img-clickable.selected').forEach(el => { el.classList.remove('selected'); + removeOrderBadge(el); }); updateCount(); updateCommonTagsDisplay(); @@ -133,7 +168,10 @@ function dispatchSelectionChanged() { document.dispatchEvent(new CustomEvent('selectionChanged', { - detail: { count: state.selectedIds.size } + detail: { + count: state.selectedIds.size, + orderedIds: [...state.selectionOrder] + } })); } @@ -165,11 +203,43 @@ } function updateSelectionUI() { - // Re-apply selected class to any newly loaded images that are in selection + // Re-apply selected class and order badges to any newly loaded images that are in selection document.querySelectorAll('.img-clickable').forEach(item => { const id = parseInt(item.dataset.id); if (state.selectedIds.has(id)) { item.classList.add('selected'); + const orderIndex = state.selectionOrder.indexOf(id); + if (orderIndex !== -1) { + addOrderBadge(item, orderIndex + 1); + } + } + }); + } + + // --------------------------- + // Order Badge Helpers + // --------------------------- + function addOrderBadge(item, orderNumber) { + removeOrderBadge(item); // Remove existing badge first + const badge = document.createElement('div'); + badge.className = 'selection-order-badge'; + badge.textContent = orderNumber; + item.appendChild(badge); + } + + function removeOrderBadge(item) { + const existing = item.querySelector('.selection-order-badge'); + if (existing) existing.remove(); + } + + function updateAllOrderBadges() { + document.querySelectorAll('.img-clickable').forEach(item => { + const id = parseInt(item.dataset.id); + const orderIndex = state.selectionOrder.indexOf(id); + if (orderIndex !== -1) { + addOrderBadge(item, orderIndex + 1); + } else { + removeOrderBadge(item); } }); } @@ -474,6 +544,7 @@ // Expose global API for other scripts (like series editor) window.selectedImages = state.selectedIds; + window.selectionOrder = state.selectionOrder; // Ordered array of selected IDs window.clearSelection = clearSelection; window.enableSelectMode = enableSelectMode; window.disableSelectMode = disableSelectMode; diff --git a/app/static/style.css b/app/static/style.css index 44ca38d..223c548 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -2036,6 +2036,55 @@ body.select-mode .gallery-infinite-container { font-size: 0.85rem; } +/* Floating preview overlay (for select mode hover) */ +.floating-preview { + display: none; + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + width: 600px; + height: 500px; + background: var(--panel); + border: 2px solid var(--accent); + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + padding: 4px; + pointer-events: none; +} +.floating-preview.active { + display: flex; + align-items: center; + justify-content: center; +} +.floating-preview img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 4px; +} + +/* Selection order badge on gallery items */ +.selection-order-badge { + position: absolute; + top: 4px; + left: 4px; + width: 24px; + height: 24px; + background: var(--accent); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: bold; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + z-index: 10; + pointer-events: none; +} + /* Mobile: full-width panel from bottom */ @media (max-width: 640px) { .bulk-editor-panel { diff --git a/app/templates/gallery.html b/app/templates/gallery.html index a463fcb..a541efa 100644 --- a/app/templates/gallery.html +++ b/app/templates/gallery.html @@ -120,6 +120,11 @@ {% include '_gallery_modal.html' %} + +