Expanded UI

This commit is contained in:
Aodhan
2025-06-21 21:44:52 +01:00
parent 324a21800a
commit 1ff4a6f6d7
89 changed files with 81619 additions and 114 deletions

716
js/enhanced-history.js Normal file
View File

@@ -0,0 +1,716 @@
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();
});

232
js/enhanced-main.js Normal file
View File

@@ -0,0 +1,232 @@
import { showToast, updateImageInfo } from './utils.js';
import SwipeCard from '../components/swipe-card.js';
document.addEventListener('DOMContentLoaded', () => {
// Track total images and processed count for progress bar
const progressState = {
totalImages: 0,
processedImages: 0
};
// Global state
const state = {
currentImageInfo: null,
currentOrientation: 'all',
isLoading: false
};
// DOM elements
const lastActionText = document.getElementById('last-action');
const orientationFilters = document.querySelector('.orientation-filters');
const modal = document.getElementById('fullscreen-modal');
const fullscreenImage = document.getElementById('fullscreen-image');
const closeModal = document.querySelector('.close-modal');
const progressBar = document.getElementById('progress-bar');
// Initialize the enhanced swipe card
const swipeCard = new SwipeCard({
container: document.querySelector('.swipe-container'),
onSwipe: performSwipe,
threshold: 100
});
// Make state available to window for debugging and other components
window.state = state;
window.performSwipe = performSwipe;
function performSwipe(direction) {
if (!state.currentImageInfo) return;
// Update last action text with the action name instead of direction
const actionMap = {
left: 'Discarded',
right: 'Kept',
up: 'Favorited',
down: 'Marked for review'
};
lastActionText.textContent = `Last action: ${actionMap[direction] || 'Unknown'}`;
// Show toast notification
const toastMap = {
left: 'Discarded',
right: 'Kept',
up: 'Favorited',
down: 'Marked for review'
};
showToast(toastMap[direction] || 'Action');
// Record the selection
recordSelection(state.currentImageInfo, direction);
// Update progress
progressState.processedImages++;
updateProgressBar();
// Load new image after animation completes
setTimeout(() => {
loadNewImage();
}, 500);
}
function updateProgressBar() {
if (progressState.totalImages > 0) {
const percentage = (progressState.processedImages / progressState.totalImages) * 100;
progressBar.style.width = `${Math.min(percentage, 100)}%`;
}
}
function recordSelection(imageInfo, action) {
fetch('/selection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_path: imageInfo.path,
resolution: imageInfo.resolution,
action,
}),
}).catch(error => console.error('Error recording selection:', error));
}
function loadNewImage() {
if (state.isLoading) return;
state.isLoading = true;
swipeCard.showLoading();
// First, get the total count if we don't have it yet
if (progressState.totalImages === 0) {
fetch('/image-count')
.then(response => response.json())
.catch(() => ({ count: 100 })) // Fallback if endpoint doesn't exist
.then(data => {
progressState.totalImages = data.count || 100;
updateProgressBar();
});
}
fetch(`/random-image?orientation=${state.currentOrientation}&t=${new Date().getTime()}`)
.then(response => response.json())
.then(data => {
state.isLoading = false;
swipeCard.hideLoading();
if (data && data.path) {
state.currentImageInfo = data;
swipeCard.setImage(data);
updateImageInfo(data);
adjustContainerToImage(data.orientation);
} else {
swipeCard.card.innerHTML = `<div class="no-images-message">${data.message || 'No more images.'}</div>`;
state.currentImageInfo = null;
}
})
.catch(error => {
console.error('Error fetching image:', error);
state.isLoading = false;
swipeCard.hideLoading();
swipeCard.card.innerHTML = '<div class="no-images-message">Error loading image.</div>';
});
}
function adjustContainerToImage(orientation) {
const container = document.querySelector('.swipe-container');
if (window.innerWidth < 992) { // Only on desktop
container.style.transition = 'all 0.5s ease-in-out';
if (orientation === 'landscape') {
container.style.flex = '4';
} else {
container.style.flex = '2';
}
}
}
// Button event listeners
document.getElementById('btn-left').addEventListener('click', () => performSwipe('left'));
document.getElementById('btn-right').addEventListener('click', () => performSwipe('right'));
document.getElementById('btn-up').addEventListener('click', () => performSwipe('up'));
document.getElementById('btn-down').addEventListener('click', () => performSwipe('down'));
// Orientation filter event listeners
orientationFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON' && !e.target.classList.contains('active')) {
orientationFilters.querySelector('.active').classList.remove('active');
e.target.classList.add('active');
state.currentOrientation = e.target.dataset.orientation;
loadNewImage();
}
});
// Modal event listeners
swipeCard.card.addEventListener('click', () => {
if (!swipeCard.state.hasMoved && state.currentImageInfo) {
fullscreenImage.src = state.currentImageInfo.path;
document.getElementById('modal-resolution').textContent = `Resolution: ${state.currentImageInfo.resolution}`;
document.getElementById('modal-filename').textContent = `Filename: ${state.currentImageInfo.filename || 'N/A'}`;
document.getElementById('modal-creation-date').textContent = `Creation Date: ${state.currentImageInfo.creation_date || 'N/A'}`;
modal.style.display = 'flex';
// Add animation classes
setTimeout(() => {
modal.classList.add('show');
}, 10);
}
});
closeModal.addEventListener('click', () => {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 400);
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 400);
}
});
// Keyboard event listeners
document.addEventListener('keydown', (e) => {
if (modal.style.display === 'flex' && e.key === 'Escape') {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = 'none';
}, 400);
return;
}
if (modal.style.display !== 'flex') {
switch (e.key) {
case 'ArrowLeft': performSwipe('left'); break;
case 'ArrowRight': performSwipe('right'); break;
case 'ArrowUp': performSwipe('up'); break;
case 'ArrowDown': performSwipe('down'); break;
}
}
});
// Add ripple effect to action buttons
document.querySelectorAll('.action-btn').forEach(button => {
button.addEventListener('click', function(e) {
const ripple = document.createElement('span');
ripple.classList.add('ripple');
this.appendChild(ripple);
const rect = button.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
ripple.style.width = ripple.style.height = `${size}px`;
ripple.style.left = `${e.clientX - rect.left - size/2}px`;
ripple.style.top = `${e.clientY - rect.top - size/2}px`;
setTimeout(() => {
ripple.remove();
}, 600);
});
});
// Initialize by loading the first image
loadNewImage();
});

View File

@@ -1,7 +1,11 @@
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');
// Unified filter state
const filterState = {
action: 'all',
orientation: 'all',
resolution: 'all'
};
const resolutionFilter = document.getElementById('resolution-filter');
const selectAllBtn = document.getElementById('select-all');
const deselectAllBtn = document.getElementById('deselect-all');
@@ -19,9 +23,7 @@ document.addEventListener('DOMContentLoaded', function() {
const cancelResetBtn = document.getElementById('cancel-reset');
const resetMessage = document.getElementById('reset-message');
let currentFilter = 'all';
let currentOrientation = 'all';
let currentResolution = 'all';
let cachedSelections = [];
let selectedItems = [];
let currentSelectionId = null;
@@ -32,8 +34,9 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.selections && data.selections.length > 0) {
populateResolutionFilter(data.selections);
renderSelections(data.selections);
cachedSelections = data.selections;
populateResolutionFilter(cachedSelections);
renderSelections();
} else {
selectionGrid.innerHTML = '<div class="no-selections">No selections found</div>';
}
@@ -55,13 +58,14 @@ document.addEventListener('DOMContentLoaded', function() {
});
};
const renderSelections = (selections) => {
const renderSelections = () => {
selectionGrid.innerHTML = '';
const selections = cachedSelections;
const filteredSelections = selections.filter(s =>
(currentFilter === 'all' || s.action === currentFilter) &&
(currentOrientation === 'all' || s.orientation === currentOrientation) &&
(currentResolution === 'all' || s.resolution === currentResolution)
(filterState.action === 'all' || s.action === filterState.action) &&
(filterState.orientation === 'all' || s.orientation === filterState.orientation) &&
(filterState.resolution === 'all' || s.resolution === filterState.resolution)
);
if (filteredSelections.length === 0) {
@@ -78,7 +82,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="checkbox" class="selection-checkbox">
</div>
<img src="${selection.image_path}" alt="Selected image" loading="lazy">
<div class="selection-action action-${selection.action}">${getActionName(selection.action)}</div>
<div class="selection-action action-${actionClass(selection.action)}">${selection.action}</div>
<div class="selection-info">
<p>${selection.image_path.split('/').pop()}</p>
<p>Resolution: ${selection.resolution}</p>
@@ -92,10 +96,11 @@ document.addEventListener('DOMContentLoaded', function() {
});
};
const getActionName = (action) => {
const names = { left: 'Discard', right: 'Keep', up: 'Favorite', down: 'Review' };
return names[action] || action;
const actionClass = (action) => {
const map = { 'Discard':'discard', 'Keep':'keep', 'Favorite':'favorite', 'Review':'review' };
return map[action] || 'discard';
};
const getActionName = (action) => action;
const updateDownloadButton = () => {
downloadSelectedBtn.disabled = selectedItems.length === 0;
@@ -130,23 +135,29 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
filterButtons.forEach(button => button.addEventListener('click', function() {
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
currentFilter = this.dataset.filter;
loadSelections();
}));
// Delegated click handler for any filter button
document.querySelector('.filter-container').addEventListener('click', (e) => {
const btn = e.target.closest('.filter-btn');
if (!btn) return;
orientationButtons.forEach(button => button.addEventListener('click', function() {
orientationButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
currentOrientation = this.dataset.orientation;
loadSelections();
}));
// Determine filter type and value
const { filter, orientation } = btn.dataset;
if (filter !== undefined) {
filterState.action = filter;
// update active classes within the same group
btn.parentElement.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
}
if (orientation !== undefined) {
filterState.orientation = orientation;
btn.parentElement.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
}
btn.classList.add('active');
renderSelections();
});
resolutionFilter.addEventListener('change', function() {
currentResolution = this.value;
loadSelections();
filterState.resolution = this.value;
renderSelections();
});
selectAllBtn.addEventListener('click', () => {

View File

@@ -27,11 +27,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (!state.currentImageInfo) return;
card.classList.add(`swipe-${direction}`);
lastActionText.textContent = `Last action: Swiped ${direction}`;
const actionNameMap = { left: 'Discard', right: 'Keep', up: 'Favorite', down: 'Review' };
const actionName = actionNameMap[direction] || direction;
lastActionText.textContent = `Last action: ${actionName}`;
const toastMap = { left: 'Discarded', right: 'Kept', up: 'Favorited', down: 'Marked for review' };
showToast(toastMap[direction] || 'Action');
recordSelection(state.currentImageInfo, direction);
recordSelection(state.currentImageInfo, actionName);
setTimeout(() => {
card.classList.remove(`swipe-${direction}`);
@@ -39,16 +41,25 @@ document.addEventListener('DOMContentLoaded', () => {
}, 500);
};
const recordSelection = (imageInfo, action) => {
fetch('/selection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_path: imageInfo.path,
resolution: imageInfo.resolution,
action,
}),
}).catch(error => console.error('Error recording selection:', error));
const recordSelection = async (imageInfo, action) => {
try {
const response = await fetch('/selection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_path: imageInfo.path,
action,
}),
});
if (!response.ok) {
console.error('Error recording selection. Status:', response.status);
} else {
const data = await response.json();
console.log('Selection recorded:', data);
}
} catch (err) {
console.error('Error recording selection:', err);
}
};
const loadNewImage = () => {
@@ -60,16 +71,26 @@ document.addEventListener('DOMContentLoaded', () => {
.then(response => response.json())
.then(data => {
state.isLoading = false;
card.classList.remove('loading');
// card.classList.remove('loading'); // moved to image load handler
if (data && data.path) {
state.currentImageInfo = data;
const cardImage = card.querySelector('img');
// Use load event to ensure indicator hides after image fully loads
cardImage.onload = () => {
card.classList.remove('loading');
};
cardImage.src = data.path;
updateImageInfo(data);
adjustContainerToImage(data.orientation);
} else {
card.innerHTML = `<div class="no-images-message">${data.message || 'No more images.'}</div>`;
state.currentImageInfo = null;
const placeholder = 'static/no-image.png';
const imgEl = card.querySelector('img');
if (imgEl) {
imgEl.onload = () => card.classList.remove('loading');
imgEl.src = placeholder;
}
updateImageInfo({ filename:'No image', creation_date:'', resolution:'', prompt_data:''});
state.currentImageInfo = null; // disables swipe actions
}
})
.catch(error => {

195
js/ui-enhancements.js Normal file
View File

@@ -0,0 +1,195 @@
/**
* UI Enhancements for the Swiper App
* Adds improved visual feedback and animations
*/
// Add swipe decision indicators
function addSwipeDecisionIndicators() {
const swipeContainer = document.querySelector('.swipe-container');
// Create decision indicators for each direction
const directions = ['left', 'right', 'up', 'down'];
const icons = ['fa-trash', 'fa-folder-plus', 'fa-star', 'fa-clock'];
directions.forEach((direction, index) => {
const indicator = document.createElement('div');
indicator.className = `swipe-decision decision-${direction}`;
indicator.innerHTML = `<i class="fa-solid ${icons[index]} fa-bounce"></i>`;
swipeContainer.appendChild(indicator);
});
}
// Enhance loading indicator
function enhanceLoadingIndicator() {
const loadingIndicator = document.querySelector('.loading-indicator');
if (loadingIndicator) {
loadingIndicator.innerHTML = `
<div class="loading-spinner"></div>
<div>Loading next image...</div>
`;
}
}
// Add hover effects to direction arrows
function enhanceDirectionArrows() {
const arrows = document.querySelectorAll('.direction-arrow');
arrows.forEach(arrow => {
arrow.addEventListener('mouseenter', function() {
this.style.transform = this.classList.contains('arrow-left') || this.classList.contains('arrow-right')
? `translateY(-50%) scale(1.2)`
: `translateX(-50%) scale(1.2)`;
this.style.boxShadow = '0 10px 25px rgba(0, 0, 0, 0.2)';
});
arrow.addEventListener('mouseleave', function() {
this.style.transform = this.classList.contains('arrow-left') || this.classList.contains('arrow-right')
? `translateY(-50%) scale(1)`
: `translateX(-50%) scale(1)`;
this.style.boxShadow = '0 5px 15px rgba(0, 0, 0, 0.1)';
});
});
}
// Show swipe decision indicator
function showSwipeDecision(direction) {
const indicator = document.querySelector(`.decision-${direction}`);
if (indicator) {
indicator.classList.add('visible');
setTimeout(() => {
indicator.classList.remove('visible');
}, 800);
}
}
// Enhance the performSwipe function
function enhancePerformSwipe() {
// Store the original performSwipe function
const originalPerformSwipe = window.performSwipe;
if (typeof originalPerformSwipe === 'function') {
// Override with enhanced version
window.performSwipe = function(direction) {
// Show the decision indicator
showSwipeDecision(direction);
// Call the original function
return originalPerformSwipe(direction);
};
}
}
// Add card tilt effect based on mouse position
function addCardTiltEffect() {
const card = document.getElementById('current-card');
const container = document.querySelector('.swipe-container');
if (!card || !container) return;
container.addEventListener('mousemove', e => {
if (window.innerWidth < 992) return; // Skip on mobile
// Only apply when not dragging
if (window.state && window.state.isDragging) return;
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Calculate rotation based on mouse position
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const rotateY = ((x - centerX) / centerX) * 5; // Max 5 degrees
const rotateX = ((centerY - y) / centerY) * 5; // Max 5 degrees
// Apply the transform
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
});
container.addEventListener('mouseleave', () => {
// Reset transform when mouse leaves
card.style.transform = '';
});
}
// Add pulse effect to action buttons
function addButtonPulseEffect() {
const buttons = document.querySelectorAll('.action-btn');
buttons.forEach(button => {
button.addEventListener('mouseenter', function() {
this.classList.add('pulse');
});
button.addEventListener('mouseleave', function() {
this.classList.remove('pulse');
});
});
// Add the pulse animation to CSS
const style = document.createElement('style');
style.textContent = `
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.action-btn.pulse {
animation: pulse 1s infinite;
}
`;
document.head.appendChild(style);
}
// Enhance the history page items
function enhanceHistoryItems() {
if (!window.location.pathname.includes('history')) return;
// Add hover effect to selection items
const items = document.querySelectorAll('.selection-item');
items.forEach(item => {
// Add image zoom effect on hover
const img = item.querySelector('img');
if (img) {
img.style.transition = 'transform 0.3s ease';
item.addEventListener('mouseenter', () => {
img.style.transform = 'scale(1.1)';
});
item.addEventListener('mouseleave', () => {
img.style.transform = '';
});
}
});
}
// Initialize all UI enhancements
function initUIEnhancements() {
// Add a small delay to ensure DOM is fully loaded
setTimeout(() => {
addSwipeDecisionIndicators();
enhanceLoadingIndicator();
enhanceDirectionArrows();
enhancePerformSwipe();
addCardTiltEffect();
addButtonPulseEffect();
enhanceHistoryItems();
console.log('UI enhancements initialized');
}, 500);
}
// Run when DOM is loaded
document.addEventListener('DOMContentLoaded', initUIEnhancements);
// Export functions for potential use in other modules
export {
showSwipeDecision,
addSwipeDecisionIndicators,
enhanceLoadingIndicator
};

View File

@@ -11,4 +11,21 @@ export function updateImageInfo(data) {
if (resolutionEl) {
resolutionEl.textContent = `Resolution: ${data.resolution || 'N/A'}`;
}
const filenameEl = document.getElementById('image-filename');
if (filenameEl) {
filenameEl.textContent = `Filename: ${data.filename || 'N/A'}`;
}
const creationEl = document.getElementById('image-creation-date');
if (creationEl) {
creationEl.textContent = `Created: ${data.creation_date || 'N/A'}`;
}
const promptEl = document.getElementById('image-prompt');
if (promptEl) {
// textarea uses value attribute
if (promptEl.tagName === 'TEXTAREA') {
promptEl.value = data.prompt_data || '';
} else {
promptEl.textContent = data.prompt_data || '';
}
}
}