Files
swiper/js/enhanced-history.js
2025-06-21 21:44:52 +01:00

717 lines
28 KiB
JavaScript

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 = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Loading selections...</div>
</div>
`;
}
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 = `
<div class="no-selections">
<i class="fa-solid fa-image-slash fa-3x"></i>
<p>No selections found</p>
</div>
`;
}
})
.catch(error => {
hideLoading();
console.error('Error loading selections:', error);
selectionGrid.innerHTML = `
<div class="error">
<i class="fa-solid fa-triangle-exclamation fa-3x"></i>
<p>Error loading selections: ${error.message}</p>
</div>
`;
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 = `
<div class="stat-item">
<span class="stat-value">${stats.total}</span>
<span class="stat-label">Total Images</span>
</div>
<div class="stat-item">
<span class="stat-value">${stats.byAction.left}</span>
<span class="stat-label">Discarded</span>
</div>
<div class="stat-item">
<span class="stat-value">${stats.byAction.right}</span>
<span class="stat-label">Kept</span>
</div>
<div class="stat-item">
<span class="stat-value">${stats.byAction.up}</span>
<span class="stat-label">Favorited</span>
</div>
<div class="stat-item">
<span class="stat-value">${stats.byAction.down}</span>
<span class="stat-label">For Review</span>
</div>
`;
// 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 = '<option value="all">All Resolutions</option>';
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 = `
<div class="no-selections">
<i class="fa-solid fa-filter-circle-xmark fa-3x"></i>
<p>No selections match the current filters</p>
</div>
`;
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 = `
<div class="selection-checkbox-container">
<input type="checkbox" class="selection-checkbox" id="checkbox-${selection.id}">
<label for="checkbox-${selection.id}" class="checkbox-label"></label>
</div>
<div class="image-container">
<img src="${selection.image_path}" alt="${actionName} image" loading="lazy">
</div>
<div class="selection-action action-${selection.action}">
<i class="fa-solid ${actionIcon}"></i> ${actionName}
</div>
<div class="selection-info">
<p class="filename">${selection.image_path.split('/').pop()}</p>
<p class="resolution">Resolution: ${selection.resolution}</p>
<p class="timestamp">Date: ${formatDate(selection.timestamp)}</p>
</div>
<div class="selection-controls">
<button class="control-btn edit-btn" title="Change action">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button class="control-btn view-btn" title="View full size">
<i class="fa-solid fa-expand"></i>
</button>
<button class="control-btn delete-btn" title="Remove">
<i class="fa-solid fa-trash"></i>
</button>
</div>
`;
// 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 = `
<div class="modal-content">
<span class="close-modal">&times;</span>
<img src="${selection.image_path}" alt="Full size image">
<div class="modal-info">
<p>Action: <span class="action-${selection.action}">${getActionName(selection.action)}</span></p>
<p>Filename: ${selection.image_path.split('/').pop()}</p>
</div>
</div>
`;
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 = `
<div class="modal-content">
<h3>Confirm Deletion</h3>
<p>Are you sure you want to delete this selection?</p>
<div class="confirmation-buttons">
<button class="action-btn confirm-delete-btn">Yes, Delete</button>
<button class="action-btn cancel-delete-btn">Cancel</button>
</div>
</div>
`;
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 = `<i class="fa-solid ${getActionIcon(action)}"></i> ${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 = `
<div class="no-selections">
<i class="fa-solid fa-image-slash fa-3x"></i>
<p>No selections found</p>
</div>
`;
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();
});