Files
swiper/history.html
Aodhan c09461f58f Enhance image swipe app mobile experience
Major improvements:
- Added responsive mobile view with optimized touch interactions
- Implemented image caching to preload up to 2 images for faster transitions
- Made images enter consistently from left side regardless of swipe direction
- Enhanced swipe animations with reduced tilt and better fade-out effects
- Reduced swipe sensitivity on mobile for better tap/swipe distinction
- Removed headings and reduced history button height for more screen space
- Added progressive fade effect during manual swipes
- Sped up slide-in animations for snappier experience
- Fixed multiple edge cases for better overall stability
2025-05-29 01:31:26 +01:00

878 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Selection History</title>
<link rel="stylesheet" href="styles.css">
<style>
.history-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-buttons {
display: flex;
gap: 10px;
}
.back-button {
padding: 8px 15px;
background-color: #333;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
.filter-container {
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 20px;
background-color: #f5f5f5;
padding: 15px;
border-radius: 8px;
}
.filter-section {
flex: 1;
min-width: 200px;
}
.filter-section h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
background-color: #ddd;
color: #333;
transition: all 0.2s;
}
.filter-btn.active {
background-color: #2c3e50;
color: white;
}
.resolution-select {
width: 100%;
padding: 6px;
border-radius: 4px;
border: 1px solid #ddd;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.reset-btn {
padding: 8px 15px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.select-btn {
padding: 8px 15px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.download-btn {
padding: 8px 15px;
background-color: #2ecc71;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.download-btn:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.selection-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.selection-item {
border-radius: 10px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
background-color: white;
position: relative;
transition: transform 0.2s, box-shadow 0.2s;
}
.selection-item.selected {
box-shadow: 0 0 0 3px #3498db, 0 3px 10px rgba(0, 0, 0, 0.2);
transform: translateY(-3px);
}
.selection-checkbox-container {
position: absolute;
top: 10px;
left: 10px;
z-index: 5;
}
.selection-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.selection-item img {
width: 100%;
height: 200px;
object-fit: contain;
display: block;
}
.selection-info {
padding: 10px;
font-size: 0.9rem;
}
.selection-action {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 10px;
border-radius: 20px;
color: white;
font-weight: bold;
}
.selection-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: space-around;
padding: 8px 0;
opacity: 0;
transition: opacity 0.3s;
}
.selection-item:hover .selection-controls {
opacity: 1;
}
.control-btn {
background: none;
border: none;
color: white;
cursor: pointer;
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 3px;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.edit-btn {
color: #1e90ff;
}
.delete-btn {
color: #ff4757;
}
.action-left {
background-color: #ff4757;
}
.action-right {
background-color: #2ed573;
}
.action-up {
background-color: #1e90ff;
}
.action-down {
background-color: #ffa502;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
overflow: auto;
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
position: relative;
}
.close-modal {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: #666;
}
.action-btn {
padding: 8px 15px;
border: none;
border-radius: 5px;
color: white;
font-weight: bold;
cursor: pointer;
margin: 0 5px;
}
.no-selections {
text-align: center;
padding: 30px;
color: #666;
font-size: 1.2rem;
}
</style>
</head>
<body>
<!-- Action change modal -->
<div id="action-modal" class="modal">
<div class="modal-content" style="max-width: 400px; height: auto;">
<span class="close-modal" id="close-action-modal">&times;</span>
<h2>Change Action</h2>
<div id="modal-image-preview" style="margin: 15px 0; text-align: center;">
<img id="modal-preview-img" src="" alt="Image preview" style="max-height: 200px; max-width: 100%;">
</div>
<div class="action-buttons" style="margin: 20px 0;">
<button class="action-btn" data-action="left" style="background-color: #ff4757;">Discard</button>
<button class="action-btn" data-action="right" style="background-color: #2ed573;">Keep</button>
<button class="action-btn" data-action="up" style="background-color: #1e90ff;">Favorite</button>
<button class="action-btn" data-action="down" style="background-color: #ffa502;">Review</button>
</div>
<div id="modal-message" style="text-align: center; margin-top: 10px; color: #666;"></div>
</div>
</div>
<div class="history-container">
<div class="history-header">
<h1>Image Selection History</h1>
<div class="header-buttons">
<a href="/" class="back-button">Back to Swipe</a>
</div>
</div>
<!-- Reset confirmation modal -->
<div id="reset-modal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 400px; height: auto;">
<h2>Reset Database</h2>
<p>Are you sure you want to delete all selections? This cannot be undone.</p>
<div class="reset-modal-buttons">
<button id="confirm-reset" class="danger-button">Yes, Delete All</button>
<button id="cancel-reset" class="cancel-button">Cancel</button>
</div>
<div id="reset-message" style="text-align: center; margin-top: 15px; color: #666;"></div>
</div>
</div>
<div class="filter-container">
<div class="filter-section">
<h4>Action</h4>
<div class="filter-buttons">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="left">Discarded</button>
<button class="filter-btn" data-filter="right">Kept</button>
<button class="filter-btn" data-filter="up">Favorited</button>
<button class="filter-btn" data-filter="down">Review Later</button>
</div>
</div>
<div class="filter-section">
<h4>Orientation</h4>
<div class="filter-buttons orientation-filters">
<button class="filter-btn active" data-orientation="all">All</button>
<button class="filter-btn" data-orientation="portrait">Portrait</button>
<button class="filter-btn" data-orientation="landscape">Landscape</button>
<button class="filter-btn" data-orientation="square">Square</button>
</div>
</div>
<div class="filter-section">
<h4>Resolution</h4>
<select id="resolution-filter" class="resolution-select">
<option value="all">All Resolutions</option>
<!-- This will be populated dynamically -->
</select>
</div>
<div class="action-buttons">
<button id="reset-db" class="reset-btn">Reset Database</button>
<button id="select-all" class="select-btn">Select All</button>
<button id="deselect-all" class="select-btn">Deselect All</button>
<button id="download-selected" class="download-btn" disabled>Download Selected</button>
</div>
</div>
<div id="selection-grid" class="selection-grid">
<!-- Selection items will be loaded here dynamically -->
<div class="no-selections">Loading selections...</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
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');
// Action modal elements
const actionModal = document.getElementById('action-modal');
const closeActionModal = document.getElementById('close-action-modal');
const actionButtons = document.querySelectorAll('.action-btn');
const modalPreviewImg = document.getElementById('modal-preview-img');
const modalMessage = document.getElementById('modal-message');
// Reset modal elements
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');
// State variables
let currentFilter = 'all';
let currentOrientation = 'all';
let currentResolution = 'all';
let selectedItems = [];
let currentSelectionId = null;
// Load selections on page load
loadSelections();
// Action filter button click handlers
filterButtons.forEach(button => {
if (button.dataset.filter) {
button.addEventListener('click', function() {
// Remove active class from all action filter buttons
filterButtons.forEach(btn => {
if (btn.dataset.filter) btn.classList.remove('active');
});
// Add active class to clicked button
this.classList.add('active');
// Update current filter
currentFilter = this.dataset.filter;
// Reload selections with new filter
loadSelections();
});
}
});
// Orientation filter button click handlers
orientationButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all orientation filter buttons
orientationButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Update current orientation filter
currentOrientation = this.dataset.orientation;
// Reload selections with new filter
loadSelections();
});
});
// Resolution filter change handler
resolutionFilter.addEventListener('change', function() {
currentResolution = this.value;
loadSelections();
});
// Select All button handler
selectAllBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('.selection-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
const selectionItem = checkbox.closest('.selection-item');
selectionItem.classList.add('selected');
// Add to selected items if not already there
const id = selectionItem.dataset.id;
if (!selectedItems.some(item => item.id === id)) {
// Get the image path from the img element
let imagePath = selectionItem.querySelector('img').src;
// Convert absolute URL to relative path
if (imagePath.includes('localhost')) {
// Extract just the path portion from the full URL
const url = new URL(imagePath);
imagePath = url.pathname;
}
const selection = {
id: id,
image_path: imagePath,
action: selectionItem.dataset.action,
resolution: selectionItem.dataset.resolution,
orientation: selectionItem.dataset.orientation
};
selectedItems.push(selection);
}
});
updateDownloadButton();
});
// Deselect All button handler
deselectAllBtn.addEventListener('click', function() {
const checkboxes = document.querySelectorAll('.selection-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
checkbox.closest('.selection-item').classList.remove('selected');
});
selectedItems = [];
updateDownloadButton();
});
// Download Selected button handler
downloadSelectedBtn.addEventListener('click', function() {
if (selectedItems.length === 0) return;
// Create URL with image paths
const paths = selectedItems.map(item => {
let path = item.image_path;
// Ensure path starts with /images/
if (!path.startsWith('/images/')) {
path = `/images/${path}`;
}
return path;
});
const queryString = paths.map(path => `paths=${encodeURIComponent(path)}`).join('&');
const downloadUrl = `/download-selected?${queryString}`;
// Create a temporary link and click it to trigger download
const link = document.createElement('a');
link.href = downloadUrl;
link.download = 'selected_images.zip';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// Close action modal handler
closeActionModal.addEventListener('click', function() {
actionModal.style.display = 'none';
});
// Action button click handlers
actionButtons.forEach(button => {
button.addEventListener('click', function() {
const action = this.dataset.action;
updateSelectionAction(currentSelectionId, action);
});
});
// Reset button click handler
resetBtn.addEventListener('click', function() {
resetModal.style.display = 'block';
resetMessage.textContent = '';
});
// Confirm reset button handler
confirmResetBtn.addEventListener('click', function() {
resetDatabase();
});
// Cancel reset button handler
cancelResetBtn.addEventListener('click', function() {
resetModal.style.display = 'none';
});
// Close modals when clicking outside
window.addEventListener('click', function(event) {
if (event.target === actionModal) {
actionModal.style.display = 'none';
}
if (event.target === resetModal) {
resetModal.style.display = 'none';
}
});
// Function to load selections from the server
function loadSelections() {
console.log('DEBUG: loadSelections() called');
selectionGrid.innerHTML = `<div class="no-selections">Loading selections...</div>`;
fetch('/selections')
.then(response => {
if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Selections data:', data);
if (data.selections && data.selections.length > 0) {
// Populate resolution filter
populateResolutionFilter(data.selections);
// Render the selections
renderSelections(data.selections);
} else {
selectionGrid.innerHTML = '<div class="no-selections">No selections found</div>';
downloadSelectedBtn.disabled = true;
}
})
.catch(error => {
console.error('Error loading selections:', error);
selectionGrid.innerHTML = `<div class="error">Error loading selections: ${error.message}</div>`;
downloadSelectedBtn.disabled = true;
});
}
// Function to populate resolution filter
function populateResolutionFilter(selections) {
// Get unique resolutions
const resolutions = [...new Set(selections.map(s => s.resolution))];
resolutions.sort();
// Clear existing options except 'All'
while (resolutionFilter.options.length > 1) {
resolutionFilter.remove(1);
}
// Add resolution options
resolutions.forEach(resolution => {
const option = document.createElement('option');
option.value = resolution;
option.textContent = resolution;
resolutionFilter.appendChild(option);
});
}
// Function to render selections
function renderSelections(selections) {
// Clear the grid
selectionGrid.innerHTML = '';
// Apply all filters
let filteredSelections = selections;
// Filter by action
if (currentFilter !== 'all') {
filteredSelections = filteredSelections.filter(s => s.action === currentFilter);
}
// Filter by orientation
if (currentOrientation !== 'all') {
filteredSelections = filteredSelections.filter(s => s.orientation === currentOrientation);
}
// Filter by resolution
if (currentResolution !== 'all') {
filteredSelections = filteredSelections.filter(s => s.resolution === currentResolution);
}
if (filteredSelections.length === 0) {
selectionGrid.innerHTML = '<div class="no-selections">No selections match the current filters</div>';
downloadSelectedBtn.disabled = true;
return;
}
// Reset selected items
selectedItems = [];
updateDownloadButton();
// Create and append selection items
filteredSelections.forEach(selection => {
const selectionItem = document.createElement('div');
selectionItem.className = 'selection-item';
selectionItem.dataset.id = selection.id;
selectionItem.dataset.action = selection.action;
selectionItem.dataset.orientation = selection.orientation || 'unknown';
selectionItem.dataset.resolution = selection.resolution;
// Create checkbox container
const checkboxContainer = document.createElement('div');
checkboxContainer.className = 'selection-checkbox-container';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'selection-checkbox';
checkbox.addEventListener('change', function() {
if (this.checked) {
selectionItem.classList.add('selected');
selectedItems.push(selection);
} else {
selectionItem.classList.remove('selected');
selectedItems = selectedItems.filter(item => item.id !== selection.id);
}
updateDownloadButton();
});
checkboxContainer.appendChild(checkbox);
// Create image container
const imgContainer = document.createElement('div');
imgContainer.className = 'selection-image-container';
// Create image
const img = document.createElement('img');
// Ensure image path starts with /images/
let imagePath = selection.image_path;
if (!imagePath.startsWith('/images/')) {
imagePath = `/images/${imagePath}`;
}
img.src = imagePath;
img.alt = 'Selected image';
imgContainer.appendChild(img);
// Create action badge
const actionBadge = document.createElement('div');
actionBadge.className = `selection-action action-${selection.action}`;
actionBadge.textContent = getActionName(selection.action);
// Create info container
const infoContainer = document.createElement('div');
infoContainer.className = 'selection-info';
// Add path, resolution, orientation and timestamp
const pathText = document.createElement('p');
pathText.textContent = selection.image_path.split('/').pop(); // Just show filename
const resolutionText = document.createElement('p');
resolutionText.textContent = `Resolution: ${selection.resolution}`;
const orientationText = document.createElement('p');
orientationText.textContent = `Orientation: ${selection.orientation || 'Unknown'}`;
const timestampText = document.createElement('p');
const date = new Date(selection.timestamp * 1000);
timestampText.textContent = `Date: ${date.toLocaleString()}`;
infoContainer.appendChild(pathText);
infoContainer.appendChild(resolutionText);
infoContainer.appendChild(orientationText);
infoContainer.appendChild(timestampText);
// Create controls container
const controlsContainer = document.createElement('div');
controlsContainer.className = 'selection-controls';
// Create edit button
const editBtn = document.createElement('button');
editBtn.className = 'control-btn edit-btn';
editBtn.textContent = 'Change';
editBtn.addEventListener('click', function() {
openActionModal(selection);
});
// Create delete button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'control-btn delete-btn';
deleteBtn.textContent = 'Remove';
deleteBtn.addEventListener('click', function() {
deleteSelection(selection.id);
});
controlsContainer.appendChild(editBtn);
controlsContainer.appendChild(deleteBtn);
// Assemble the selection item
selectionItem.appendChild(checkboxContainer);
selectionItem.appendChild(imgContainer);
selectionItem.appendChild(actionBadge);
selectionItem.appendChild(infoContainer);
selectionItem.appendChild(controlsContainer);
selectionGrid.appendChild(selectionItem);
});
}
// Function to update download button state
function updateDownloadButton() {
if (selectedItems.length > 0) {
downloadSelectedBtn.disabled = false;
downloadSelectedBtn.textContent = `Download Selected (${selectedItems.length})`;
} else {
downloadSelectedBtn.disabled = true;
downloadSelectedBtn.textContent = 'Download Selected';
}
}
// Function to open the action change modal
function openActionModal(selection) {
console.log('DEBUG: Opening action modal for selection:', selection);
currentSelectionId = selection.id;
modalPreviewImg.src = selection.image_path;
modalMessage.textContent = '';
actionModal.style.display = 'block';
}
// Function to update a selection's action
function updateSelectionAction(id, action) {
console.log(`DEBUG: Updating selection ${id} to action ${action}`);
modalMessage.textContent = 'Updating...';
fetch('/update-selection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: id,
action: action
})
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to update selection');
}
return response.json();
})
.then(data => {
console.log('Selection updated:', data);
modalMessage.textContent = 'Updated successfully!';
// Close the modal after a short delay
setTimeout(() => {
actionModal.style.display = 'none';
loadSelections(); // Reload the selections
}, 1000);
})
.catch(error => {
console.error('Error updating selection:', error);
modalMessage.textContent = `Error: ${error.message}`;
});
}
// Function to delete a selection
function deleteSelection(id) {
console.log('DEBUG: Deleting selection with ID:', id);
if (!confirm('Are you sure you want to delete this selection?')) {
return;
}
fetch(`/delete-selection?id=${id}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to delete selection');
}
return response.json();
})
.then(data => {
console.log('Selection deleted:', data);
loadSelections(); // Reload the selections
})
.catch(error => {
console.error('Error deleting selection:', error);
alert('Error deleting selection');
});
}
// Function to reset the database
function resetDatabase() {
console.log('DEBUG: Resetting database');
resetMessage.textContent = 'Resetting...';
fetch('/reset-database', {
method: 'POST'
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to reset database');
}
return response.json();
})
.then(data => {
console.log('Database reset:', data);
resetMessage.textContent = 'Database reset successfully!';
// Close the modal after a short delay
setTimeout(() => {
resetModal.style.display = 'none';
loadSelections(); // Reload the selections
}, 1000);
})
.catch(error => {
console.error('Error resetting database:', error);
resetMessage.textContent = `Error: ${error.message}`;
});
}
// Function to get the display name for an action
function getActionName(action) {
switch(action) {
case 'left': return 'Discard';
case 'right': return 'Keep';
case 'up': return 'Favorite';
case 'down': return 'Review';
default: return action;
}
}
// Add console logging to help debug
console.log('History page script loaded');
});
</script>
</body>
</html>