import { showToast, addRippleEffect } from './utils.js'; document.addEventListener('DOMContentLoaded', function() { const selectionGrid = document.getElementById('selection-grid'); const filterButtons = document.querySelectorAll('.filter-buttons .filter-btn'); const orientationButtons = document.querySelectorAll('.orientation-filters .filter-btn'); const resolutionFilter = document.getElementById('resolution-filter'); const selectAllBtn = document.getElementById('select-all'); const deselectAllBtn = document.getElementById('deselect-all'); const downloadSelectedBtn = document.getElementById('download-selected'); const filteredCountEl = document.getElementById('filtered-count'); // Add ripple effect to all action buttons document.querySelectorAll('.action-btn').forEach(button => { addRippleEffect(button); }); const actionModal = document.getElementById('action-modal'); const closeActionModal = document.getElementById('close-action-modal'); const actionButtons = actionModal.querySelectorAll('.action-btn'); const modalPreviewImg = document.getElementById('modal-preview-img'); const modalMessage = document.getElementById('modal-message'); const resetBtn = document.getElementById('reset-db'); const resetModal = document.getElementById('reset-modal'); const confirmResetBtn = document.getElementById('confirm-reset'); const cancelResetBtn = document.getElementById('cancel-reset'); const resetMessage = document.getElementById('reset-message'); let currentFilter = 'all'; let currentOrientation = 'all'; let currentResolution = 'all'; let selectedItems = []; let currentSelectionId = null; let allSelections = []; // Enhanced loading animation function showLoading() { selectionGrid.classList.add('loading'); selectionGrid.innerHTML = `
Loading selections...
`; } function hideLoading() { selectionGrid.classList.remove('loading'); } const loadSelections = () => { showLoading(); fetch('/selections') .then(response => response.json()) .then(data => { hideLoading(); if (data.selections && data.selections.length > 0) { allSelections = data.selections; populateResolutionFilter(data.selections); renderSelections(data.selections); // Show stats const stats = calculateStats(data.selections); updateStats(stats); showToast(`Loaded ${data.selections.length} images`); } else { selectionGrid.innerHTML = `

No selections found

`; } }) .catch(error => { hideLoading(); console.error('Error loading selections:', error); selectionGrid.innerHTML = `

Error loading selections: ${error.message}

`; showToast('Error loading selections', 'error'); }); }; const calculateStats = (selections) => { const stats = { total: selections.length, byAction: { left: selections.filter(s => s.action === 'left').length, right: selections.filter(s => s.action === 'right').length, up: selections.filter(s => s.action === 'up').length, down: selections.filter(s => s.action === 'down').length }, byOrientation: { portrait: selections.filter(s => s.orientation === 'portrait').length, landscape: selections.filter(s => s.orientation === 'landscape').length, square: selections.filter(s => s.orientation === 'square').length } }; return stats; }; const updateStats = (stats) => { const statsContainer = document.getElementById('stats-container'); if (!statsContainer) return; statsContainer.innerHTML = `
${stats.total} Total Images
${stats.byAction.left} Discarded
${stats.byAction.right} Kept
${stats.byAction.up} Favorited
${stats.byAction.down} For Review
`; // Add animation to stats const statItems = statsContainer.querySelectorAll('.stat-item'); statItems.forEach((item, index) => { item.style.opacity = 0; item.style.transform = 'translateY(20px)'; setTimeout(() => { item.style.transition = 'all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; item.style.opacity = 1; item.style.transform = 'translateY(0)'; }, 100 * index); }); }; const populateResolutionFilter = (selections) => { const resolutions = [...new Set(selections.map(s => s.resolution))].sort(); resolutionFilter.innerHTML = ''; resolutions.forEach(resolution => { const option = document.createElement('option'); option.value = resolution; option.textContent = resolution; resolutionFilter.appendChild(option); }); }; const renderSelections = (selections) => { selectionGrid.innerHTML = ''; const filteredSelections = selections.filter(s => (currentFilter === 'all' || s.action === currentFilter) && (currentOrientation === 'all' || s.orientation === currentOrientation) && (currentResolution === 'all' || s.resolution === currentResolution) ); if (filteredSelections.length === 0) { selectionGrid.innerHTML = `

No selections match the current filters

`; filteredCountEl.textContent = `0 images match your filters (out of ${selections.length} total)`; return; } // Update the filtered count if (filteredCountEl) { filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${selections.length} images`; } // Create a document fragment for better performance const fragment = document.createDocumentFragment(); filteredSelections.forEach((selection, index) => { const item = document.createElement('div'); item.className = 'selection-item'; item.dataset.id = selection.id; // Add animation delay for staggered appearance const delay = Math.min(0.05 * index, 1); item.style.animationDelay = `${delay}s`; const actionName = getActionName(selection.action); const actionIcon = getActionIcon(selection.action); item.innerHTML = `
${actionName} image
${actionName}

${selection.image_path.split('/').pop()}

Resolution: ${selection.resolution}

Date: ${formatDate(selection.timestamp)}

`; // Add the item to the fragment fragment.appendChild(item); }); // Append all items at once selectionGrid.appendChild(fragment); // Add fade-in animation class setTimeout(() => { selectionGrid.classList.add('loaded'); }, 10); }; const formatDate = (timestamp) => { if (!timestamp) return 'Unknown'; const date = new Date(timestamp * 1000); return date.toLocaleDateString(); }; const getActionName = (action) => { const names = { left: 'Discard', right: 'Keep', up: 'Favorite', down: 'Review' }; return names[action] || action; }; const getActionIcon = (action) => { const icons = { left: 'fa-trash', right: 'fa-folder-plus', up: 'fa-star', down: 'fa-clock' }; return icons[action] || 'fa-question'; }; const updateDownloadButton = () => { downloadSelectedBtn.disabled = selectedItems.length === 0; downloadSelectedBtn.querySelector('.label').textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download'; }; // Enhanced selection item click handler selectionGrid.addEventListener('click', (e) => { const target = e.target; const selectionItem = target.closest('.selection-item'); if (!selectionItem) return; const selectionId = selectionItem.dataset.id; const selection = { id: selectionId, image_path: selectionItem.querySelector('img').src, action: selectionItem.querySelector('.selection-action').classList[1].replace('action-', '') }; // Handle checkbox click if (target.classList.contains('selection-checkbox') || target.classList.contains('checkbox-label')) { const checkbox = selectionItem.querySelector('.selection-checkbox'); const isChecked = checkbox.checked; if (isChecked) { selectionItem.classList.add('selected'); selectedItems.push(selection); showToast(`Selected image (${selectedItems.length} total)`); } else { selectionItem.classList.remove('selected'); selectedItems = selectedItems.filter(item => item.id !== selectionId); showToast(`Deselected image (${selectedItems.length} total)`); } updateDownloadButton(); } // Handle edit button click else if (target.classList.contains('edit-btn') || target.closest('.edit-btn')) { currentSelectionId = selectionId; modalPreviewImg.src = selection.image_path; // Highlight the current action in the modal actionButtons.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.action === selection.action) { btn.classList.add('active'); } }); actionModal.style.display = 'flex'; setTimeout(() => { actionModal.classList.add('show'); }, 10); } // Handle view button click else if (target.classList.contains('view-btn') || target.closest('.view-btn')) { // Open image in fullscreen modal const modal = document.createElement('div'); modal.className = 'modal fullscreen-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); // Show the modal with animation setTimeout(() => { modal.style.display = 'flex'; setTimeout(() => modal.classList.add('show'), 10); }, 10); // Add close functionality modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('close-modal')) { modal.classList.remove('show'); setTimeout(() => { modal.style.display = 'none'; modal.remove(); }, 400); } }); } // Handle delete button click else if (target.classList.contains('delete-btn') || target.closest('.delete-btn')) { // Create a confirmation dialog const confirmDialog = document.createElement('div'); confirmDialog.className = 'modal confirmation-modal'; confirmDialog.innerHTML = ` `; document.body.appendChild(confirmDialog); // Show the dialog with animation setTimeout(() => { confirmDialog.style.display = 'flex'; setTimeout(() => confirmDialog.classList.add('show'), 10); }, 10); // Add button functionality confirmDialog.querySelector('.confirm-delete-btn').addEventListener('click', () => { // Add animation before removing selectionItem.classList.add('removing'); // Close the dialog confirmDialog.classList.remove('show'); setTimeout(() => { confirmDialog.style.display = 'none'; confirmDialog.remove(); }, 400); // Simulate delete functionality (replace with actual API call) setTimeout(() => { selectionItem.remove(); showToast('Selection removed'); // Update selected items if this was selected if (selectedItems.some(item => item.id === selectionId)) { selectedItems = selectedItems.filter(item => item.id !== selectionId); updateDownloadButton(); } // Update allSelections array allSelections = allSelections.filter(s => s.id !== selectionId); // Update stats const stats = calculateStats(allSelections); updateStats(stats); // Update filtered count if (filteredCountEl) { const filteredSelections = allSelections.filter(s => (currentFilter === 'all' || s.action === currentFilter) && (currentOrientation === 'all' || s.orientation === currentOrientation) && (currentResolution === 'all' || s.resolution === currentResolution) ); filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${allSelections.length} images`; } }, 300); }); confirmDialog.querySelector('.cancel-delete-btn').addEventListener('click', () => { confirmDialog.classList.remove('show'); setTimeout(() => { confirmDialog.style.display = 'none'; confirmDialog.remove(); }, 400); }); } }); // Enhanced filter button click handlers filterButtons.forEach(button => button.addEventListener('click', function() { filterButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); currentFilter = this.dataset.filter; // Apply filter animation selectionGrid.classList.add('filtering'); setTimeout(() => { renderSelections(allSelections); selectionGrid.classList.remove('filtering'); }, 300); })); orientationButtons.forEach(button => button.addEventListener('click', function() { orientationButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); currentOrientation = this.dataset.orientation; // Apply filter animation selectionGrid.classList.add('filtering'); setTimeout(() => { renderSelections(allSelections); selectionGrid.classList.remove('filtering'); }, 300); })); resolutionFilter.addEventListener('change', function() { currentResolution = this.value; // Apply filter animation selectionGrid.classList.add('filtering'); setTimeout(() => { renderSelections(allSelections); selectionGrid.classList.remove('filtering'); }, 300); }); // Enhanced select/deselect all functionality selectAllBtn.addEventListener('click', () => { const checkboxes = document.querySelectorAll('.selection-checkbox'); checkboxes.forEach(cb => cb.checked = true); selectedItems = Array.from(document.querySelectorAll('.selection-item')).map(item => ({ id: item.dataset.id, image_path: item.querySelector('img').src, action: item.querySelector('.selection-action').classList[1].replace('action-', '') })); document.querySelectorAll('.selection-item').forEach(item => item.classList.add('selected')); updateDownloadButton(); showToast(`Selected all ${checkboxes.length} visible images`); }); deselectAllBtn.addEventListener('click', () => { document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = false); selectedItems = []; document.querySelectorAll('.selection-item').forEach(item => item.classList.remove('selected')); updateDownloadButton(); showToast('Deselected all images'); }); // Enhanced download functionality downloadSelectedBtn.addEventListener('click', () => { if (selectedItems.length === 0) return; // Show loading toast showToast(`Preparing ${selectedItems.length} images for download...`); const paths = selectedItems.map(item => item.image_path); const query = paths.map(p => `paths=${encodeURIComponent(p)}`).join('&'); // Add a small delay to show the loading toast setTimeout(() => { window.location.href = `/download-selected?${query}`; }, 800); }); // Enhanced modal functionality closeActionModal.addEventListener('click', () => { actionModal.classList.remove('show'); setTimeout(() => { actionModal.style.display = 'none'; }, 400); }); actionButtons.forEach(button => { // Add ripple effect addRippleEffect(button); button.addEventListener('click', function() { // Remove active class from all buttons actionButtons.forEach(btn => btn.classList.remove('active')); // Add active class to clicked button this.classList.add('active'); const action = this.dataset.action; // Show feedback modalMessage.textContent = `Updating action to ${getActionName(action)}...`; modalMessage.style.color = '#3498db'; // Simulate API call (replace with actual implementation) setTimeout(() => { modalMessage.textContent = `Action updated successfully!`; modalMessage.style.color = '#2ecc71'; // Close modal after success setTimeout(() => { actionModal.classList.remove('show'); setTimeout(() => { actionModal.style.display = 'none'; modalMessage.textContent = ''; }, 400); // Update the UI to reflect the change const selectionItem = document.querySelector(`.selection-item[data-id="${currentSelectionId}"]`); if (selectionItem) { const actionEl = selectionItem.querySelector('.selection-action'); const oldAction = actionEl.classList[1].replace('action-', ''); // Update the action element actionEl.className = `selection-action action-${action}`; actionEl.innerHTML = ` ${getActionName(action)}`; // Update the selection in allSelections const selectionIndex = allSelections.findIndex(s => s.id === currentSelectionId); if (selectionIndex !== -1) { allSelections[selectionIndex].action = action; } // Update stats const stats = calculateStats(allSelections); updateStats(stats); // Show toast notification showToast(`Updated to ${getActionName(action)}`); } }, 1000); }, 800); }); }); // Enhanced reset functionality resetBtn.addEventListener('click', () => { resetModal.style.display = 'flex'; setTimeout(() => { resetModal.classList.add('show'); }, 10); }); confirmResetBtn.addEventListener('click', () => { // Show loading state confirmResetBtn.disabled = true; confirmResetBtn.textContent = 'Deleting...'; resetMessage.textContent = 'Deleting all selections...'; resetMessage.style.color = '#3498db'; // Simulate API call (replace with actual implementation) setTimeout(() => { resetMessage.textContent = 'All selections have been deleted successfully!'; resetMessage.style.color = '#2ecc71'; // Close modal after success setTimeout(() => { resetModal.classList.remove('show'); setTimeout(() => { resetModal.style.display = 'none'; confirmResetBtn.disabled = false; confirmResetBtn.textContent = 'Yes, Delete All'; resetMessage.textContent = ''; // Clear the grid and update state selectionGrid.innerHTML = `

No selections found

`; selectedItems = []; allSelections = []; updateDownloadButton(); // Update stats const stats = calculateStats([]); updateStats(stats); // Show toast notification showToast('All selections have been deleted'); }, 400); }, 1000); }, 1500); }); cancelResetBtn.addEventListener('click', () => { resetModal.classList.remove('show'); setTimeout(() => { resetModal.style.display = 'none'; }, 400); }); // Add filtering animation styles const style = document.createElement('style'); style.textContent = ` .selection-grid.filtering { opacity: 0.6; transform: scale(0.98); transition: all 0.3s ease; } .no-selections { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 50px; color: #999; text-align: center; gap: 20px; } .loading-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 300px; gap: 20px; } .loading-text { color: #3498db; font-size: 1.2rem; } .checkbox-label { display: inline-block; width: 20px; height: 20px; background-color: white; border: 2px solid #ddd; border-radius: 4px; cursor: pointer; position: relative; transition: all 0.2s ease; } .checkbox-label:hover { border-color: #3498db; } .selection-checkbox { position: absolute; opacity: 0; } .selection-checkbox:checked + .checkbox-label { background-color: #3498db; border-color: #3498db; } .selection-checkbox:checked + .checkbox-label::after { content: '\\f00c'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-size: 12px; } .confirmation-buttons { display: flex; gap: 15px; margin-top: 20px; justify-content: center; } .confirm-delete-btn { background: var(--gradient-danger); } .cancel-delete-btn { background: var(--gradient-primary); } `; document.head.appendChild(style); // Initialize by loading selections loadSelections(); });