Expanded UI
This commit is contained in:
716
js/enhanced-history.js
Normal file
716
js/enhanced-history.js
Normal 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">×</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
232
js/enhanced-main.js
Normal 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();
|
||||
});
|
||||
@@ -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', () => {
|
||||
|
||||
51
js/main.js
51
js/main.js
@@ -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
195
js/ui-enhancements.js
Normal 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
|
||||
};
|
||||
17
js/utils.js
17
js/utils.js
@@ -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 || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user