Basic UI
This commit is contained in:
186
js/history.js
Normal file
186
js/history.js
Normal file
@@ -0,0 +1,186 @@
|
||||
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 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;
|
||||
|
||||
const loadSelections = () => {
|
||||
selectionGrid.innerHTML = `<div class="no-selections">Loading selections...</div>`;
|
||||
|
||||
fetch('/selections')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.selections && data.selections.length > 0) {
|
||||
populateResolutionFilter(data.selections);
|
||||
renderSelections(data.selections);
|
||||
} else {
|
||||
selectionGrid.innerHTML = '<div class="no-selections">No selections found</div>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading selections:', error);
|
||||
selectionGrid.innerHTML = `<div class="error">Error loading selections: ${error.message}</div>`;
|
||||
});
|
||||
};
|
||||
|
||||
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">No selections match the current filters</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
filteredSelections.forEach(selection => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'selection-item';
|
||||
item.dataset.id = selection.id;
|
||||
item.innerHTML = `
|
||||
<div class="selection-checkbox-container">
|
||||
<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-info">
|
||||
<p>${selection.image_path.split('/').pop()}</p>
|
||||
<p>Resolution: ${selection.resolution}</p>
|
||||
</div>
|
||||
<div class="selection-controls">
|
||||
<button class="control-btn edit-btn">Change</button>
|
||||
<button class="control-btn delete-btn">Remove</button>
|
||||
</div>
|
||||
`;
|
||||
selectionGrid.appendChild(item);
|
||||
});
|
||||
};
|
||||
|
||||
const getActionName = (action) => {
|
||||
const names = { left: 'Discard', right: 'Keep', up: 'Favorite', down: 'Review' };
|
||||
return names[action] || action;
|
||||
};
|
||||
|
||||
const updateDownloadButton = () => {
|
||||
downloadSelectedBtn.disabled = selectedItems.length === 0;
|
||||
downloadSelectedBtn.querySelector('.label').textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download';
|
||||
};
|
||||
|
||||
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 };
|
||||
|
||||
if (target.classList.contains('selection-checkbox')) {
|
||||
if (target.checked) {
|
||||
selectionItem.classList.add('selected');
|
||||
selectedItems.push(selection);
|
||||
} else {
|
||||
selectionItem.classList.remove('selected');
|
||||
selectedItems = selectedItems.filter(item => item.id !== selectionId);
|
||||
}
|
||||
updateDownloadButton();
|
||||
} else if (target.classList.contains('edit-btn')) {
|
||||
currentSelectionId = selectionId;
|
||||
modalPreviewImg.src = selection.image_path;
|
||||
actionModal.style.display = 'flex';
|
||||
} else if (target.classList.contains('delete-btn')) {
|
||||
if (confirm('Are you sure you want to delete this selection?')) {
|
||||
// Implement delete functionality
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
filterButtons.forEach(button => button.addEventListener('click', function() {
|
||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
currentFilter = this.dataset.filter;
|
||||
loadSelections();
|
||||
}));
|
||||
|
||||
orientationButtons.forEach(button => button.addEventListener('click', function() {
|
||||
orientationButtons.forEach(btn => btn.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
currentOrientation = this.dataset.orientation;
|
||||
loadSelections();
|
||||
}));
|
||||
|
||||
resolutionFilter.addEventListener('change', function() {
|
||||
currentResolution = this.value;
|
||||
loadSelections();
|
||||
});
|
||||
|
||||
selectAllBtn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = true);
|
||||
selectedItems = Array.from(document.querySelectorAll('.selection-item')).map(item => ({id: item.dataset.id, image_path: item.querySelector('img').src}));
|
||||
document.querySelectorAll('.selection-item').forEach(item => item.classList.add('selected'));
|
||||
updateDownloadButton();
|
||||
});
|
||||
|
||||
deselectAllBtn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = false);
|
||||
selectedItems = [];
|
||||
document.querySelectorAll('.selection-item').forEach(item => item.classList.remove('selected'));
|
||||
updateDownloadButton();
|
||||
});
|
||||
|
||||
downloadSelectedBtn.addEventListener('click', () => {
|
||||
const paths = selectedItems.map(item => item.image_path);
|
||||
const query = paths.map(p => `paths=${encodeURIComponent(p)}`).join('&');
|
||||
window.location.href = `/download-selected?${query}`;
|
||||
});
|
||||
|
||||
closeActionModal.addEventListener('click', () => actionModal.style.display = 'none');
|
||||
|
||||
actionButtons.forEach(button => button.addEventListener('click', function() {
|
||||
const action = this.dataset.action;
|
||||
// Implement update action functionality
|
||||
}));
|
||||
|
||||
resetBtn.addEventListener('click', () => resetModal.style.display = 'flex');
|
||||
confirmResetBtn.addEventListener('click', () => {
|
||||
// Implement reset database functionality
|
||||
});
|
||||
cancelResetBtn.addEventListener('click', () => resetModal.style.display = 'none');
|
||||
|
||||
loadSelections();
|
||||
});
|
||||
194
js/main.js
Normal file
194
js/main.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import { showToast, updateImageInfo } from './utils.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const state = {
|
||||
currentImageInfo: null,
|
||||
currentOrientation: 'all',
|
||||
isLoading: false,
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
moveX: 0,
|
||||
moveY: 0,
|
||||
touchStartTime: 0,
|
||||
hasMoved: false,
|
||||
};
|
||||
|
||||
const card = document.getElementById('current-card');
|
||||
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 SWIPE_THRESHOLD = 100;
|
||||
|
||||
const performSwipe = (direction) => {
|
||||
if (!state.currentImageInfo) return;
|
||||
|
||||
card.classList.add(`swipe-${direction}`);
|
||||
lastActionText.textContent = `Last action: Swiped ${direction}`;
|
||||
const toastMap = { left: 'Discarded', right: 'Kept', up: 'Favorited', down: 'Marked for review' };
|
||||
showToast(toastMap[direction] || 'Action');
|
||||
|
||||
recordSelection(state.currentImageInfo, direction);
|
||||
|
||||
setTimeout(() => {
|
||||
card.classList.remove(`swipe-${direction}`);
|
||||
loadNewImage();
|
||||
}, 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 loadNewImage = () => {
|
||||
if (state.isLoading) return;
|
||||
state.isLoading = true;
|
||||
card.classList.add('loading');
|
||||
|
||||
fetch(`/random-image?orientation=${state.currentOrientation}&t=${new Date().getTime()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
state.isLoading = false;
|
||||
card.classList.remove('loading');
|
||||
if (data && data.path) {
|
||||
state.currentImageInfo = data;
|
||||
const cardImage = card.querySelector('img');
|
||||
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;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching image:', error);
|
||||
state.isLoading = false;
|
||||
card.classList.remove('loading');
|
||||
card.innerHTML = '<div class="no-images-message">Error loading image.</div>';
|
||||
});
|
||||
};
|
||||
|
||||
const 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';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerDown = (x, y) => {
|
||||
state.isDragging = true;
|
||||
state.startX = x;
|
||||
state.startY = y;
|
||||
state.hasMoved = false;
|
||||
state.touchStartTime = Date.now();
|
||||
card.classList.add('swiping');
|
||||
};
|
||||
|
||||
const handlePointerMove = (x, y) => {
|
||||
if (!state.isDragging) return;
|
||||
|
||||
state.moveX = x - state.startX;
|
||||
state.moveY = y - state.startY;
|
||||
|
||||
if (Math.abs(state.moveX) > 10 || Math.abs(state.moveY) > 10) {
|
||||
state.hasMoved = true;
|
||||
}
|
||||
|
||||
card.style.transform = `translate(${state.moveX}px, ${state.moveY}px) rotate(${state.moveX * 0.05}deg)`;
|
||||
};
|
||||
|
||||
const handlePointerUp = () => {
|
||||
if (!state.isDragging) return;
|
||||
state.isDragging = false;
|
||||
card.classList.remove('swiping');
|
||||
|
||||
const absX = Math.abs(state.moveX);
|
||||
const absY = Math.abs(state.moveY);
|
||||
|
||||
if (state.hasMoved && (absX > SWIPE_THRESHOLD || absY > SWIPE_THRESHOLD)) {
|
||||
if (absX > absY) {
|
||||
performSwipe(state.moveX > 0 ? 'right' : 'left');
|
||||
} else {
|
||||
performSwipe(state.moveY > 0 ? 'down' : 'up');
|
||||
}
|
||||
} else {
|
||||
card.style.transform = '';
|
||||
}
|
||||
|
||||
state.moveX = 0;
|
||||
state.moveY = 0;
|
||||
};
|
||||
|
||||
card.addEventListener('mousedown', e => handlePointerDown(e.clientX, e.clientY));
|
||||
document.addEventListener('mousemove', e => handlePointerMove(e.clientX, e.clientY));
|
||||
document.addEventListener('mouseup', () => handlePointerUp());
|
||||
|
||||
card.addEventListener('touchstart', e => handlePointerDown(e.touches[0].clientX, e.touches[0].clientY), { passive: true });
|
||||
card.addEventListener('touchmove', e => handlePointerMove(e.touches[0].clientX, e.touches[0].clientY), { passive: true });
|
||||
card.addEventListener('touchend', () => handlePointerUp());
|
||||
|
||||
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'));
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
if (!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';
|
||||
}
|
||||
});
|
||||
|
||||
closeModal.addEventListener('click', () => modal.style.display = 'none');
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (modal.style.display === 'flex' && e.key === 'Escape') {
|
||||
modal.style.display = 'none';
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadNewImage();
|
||||
});
|
||||
14
js/utils.js
Normal file
14
js/utils.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export function showToast(message) {
|
||||
const toastEl = document.getElementById('toast');
|
||||
if (!toastEl) return;
|
||||
toastEl.textContent = message;
|
||||
toastEl.classList.add('show');
|
||||
setTimeout(() => toastEl.classList.remove('show'), 3000);
|
||||
}
|
||||
|
||||
export function updateImageInfo(data) {
|
||||
const resolutionEl = document.getElementById('image-resolution');
|
||||
if (resolutionEl) {
|
||||
resolutionEl.textContent = `Resolution: ${data.resolution || 'N/A'}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user