Bug fixes.
This commit is contained in:
449
js/main.js
449
js/main.js
@@ -16,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
moveY: 0,
|
||||
touchStartTime: 0,
|
||||
hasMoved: false,
|
||||
isAnimating: false, // Flag to prevent actions during animation
|
||||
};
|
||||
|
||||
const card = document.getElementById('current-card');
|
||||
@@ -31,11 +32,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const keywordPillsContainer = document.getElementById('keyword-pills-container');
|
||||
|
||||
const SWIPE_THRESHOLD = 100;
|
||||
const ACTION_ANGLE_THRESHOLD = 15; // Degrees of rotation to commit to swipe
|
||||
|
||||
function resetCardPosition(animated = true) {
|
||||
if (state.isAnimating && animated) return;
|
||||
|
||||
card.style.transition = animated ? 'transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)' : 'none';
|
||||
card.style.transform = 'translate(0, 0) rotate(0deg)';
|
||||
|
||||
if (animated) {
|
||||
state.isAnimating = true;
|
||||
card.addEventListener('transitionend', () => {
|
||||
state.isAnimating = false;
|
||||
card.style.transition = 'none';
|
||||
}, { once: true });
|
||||
} else {
|
||||
card.style.transition = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const performSwipe = (direction) => {
|
||||
if (!state.currentImageInfo) return;
|
||||
if (!state.currentImageInfo || state.isAnimating) return;
|
||||
|
||||
card.classList.add(`swipe-${direction}`);
|
||||
state.isAnimating = true;
|
||||
|
||||
const actionNameMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' };
|
||||
const actionName = actionNameMap[direction] || direction;
|
||||
lastActionText.textContent = `Last action: ${actionName}`;
|
||||
@@ -44,310 +64,247 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
recordSelection(state.currentImageInfo, actionName);
|
||||
|
||||
setTimeout(() => {
|
||||
card.classList.remove(`swipe-${direction}`);
|
||||
// Animate swipe out
|
||||
let rotation, translateX, translateY;
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
rotation = -30;
|
||||
translateX = -windowWidth;
|
||||
translateY = state.moveY;
|
||||
break;
|
||||
case 'right':
|
||||
rotation = 30;
|
||||
translateX = windowWidth;
|
||||
translateY = state.moveY;
|
||||
break;
|
||||
case 'up':
|
||||
rotation = 0;
|
||||
translateX = state.moveX;
|
||||
translateY = -windowHeight;
|
||||
break;
|
||||
case 'down':
|
||||
rotation = 0;
|
||||
translateX = state.moveX;
|
||||
translateY = windowHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
card.style.transition = 'transform 0.5s cubic-bezier(0.6, -0.28, 0.735, 0.045)';
|
||||
card.style.transform = `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg)`;
|
||||
|
||||
card.addEventListener('transitionend', () => {
|
||||
loadNewImage();
|
||||
}, 500);
|
||||
state.isAnimating = false;
|
||||
}, { once: true });
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
if (state.isLoading) return;
|
||||
state.isLoading = true;
|
||||
card.classList.add('loading');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
async function getNextImage() {
|
||||
const queryParams = new URLSearchParams({
|
||||
orientation: state.currentOrientation.join(','),
|
||||
t: new Date().getTime(),
|
||||
});
|
||||
actions: state.currentActions.join(','),
|
||||
nsfw: state.allowNsfw,
|
||||
keywords: state.searchKeywords.join(','),
|
||||
}).toString();
|
||||
|
||||
// NSFW param
|
||||
params.append('allow_nsfw', state.allowNsfw ? '1' : '0');
|
||||
|
||||
if (state.searchKeywords.length > 0) {
|
||||
params.append('search', state.searchKeywords.join(','));
|
||||
const response = await fetch(`/next-image?${queryParams}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
if (state.currentActions.length > 0) {
|
||||
params.append('actions', state.currentActions.join(','));
|
||||
}
|
||||
function recordSelection(imageInfo, action) {
|
||||
fetch('/record-selection', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: imageInfo.filename, action: action }),
|
||||
}).catch(error => console.error('Failed to record selection:', error));
|
||||
}
|
||||
|
||||
fetch(`/random-image?${params.toString()}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
state.isLoading = false;
|
||||
// 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 {
|
||||
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 => {
|
||||
console.error('Error fetching image:', error);
|
||||
state.isLoading = false;
|
||||
card.classList.remove('loading');
|
||||
card.innerHTML = '<div class="no-images-message">Error loading image.</div>';
|
||||
});
|
||||
};
|
||||
function handleNoImageAvailable() {
|
||||
const imageElement = card.querySelector('img');
|
||||
imageElement.src = `data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ENo%20images%20found%3C%2Ftext%3E%3C%2Fsvg%3E`;
|
||||
state.currentImageInfo = null;
|
||||
updateImageInfo(null);
|
||||
showToast('No more images matching your criteria.', 'info');
|
||||
}
|
||||
|
||||
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';
|
||||
async function loadNewImage() {
|
||||
if (state.isLoading) return;
|
||||
resetCardPosition(false); // Reset position without animation for new image
|
||||
state.isLoading = true;
|
||||
const loadingIndicator = card.querySelector('.loading-indicator');
|
||||
loadingIndicator.style.display = 'block';
|
||||
|
||||
try {
|
||||
const data = await getNextImage();
|
||||
if (data && data.image_data) {
|
||||
const imageUrl = `data:image/jpeg;base64,${data.image_data}`;
|
||||
const imageElement = card.querySelector('img');
|
||||
imageElement.src = imageUrl;
|
||||
state.currentImageInfo = data;
|
||||
updateImageInfo(data);
|
||||
} else {
|
||||
container.style.flex = '2';
|
||||
handleNoImageAvailable();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
showToast('Failed to load image.', 'error');
|
||||
handleNoImageAvailable();
|
||||
} finally {
|
||||
state.isLoading = false;
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handlePointerDown = (x, y) => {
|
||||
// --- Event Listeners ---
|
||||
|
||||
// Swipe and Drag
|
||||
card.addEventListener('mousedown', onDragStart);
|
||||
card.addEventListener('touchstart', onDragStart, { passive: true });
|
||||
|
||||
function onDragStart(e) {
|
||||
if (state.isLoading || state.isAnimating) return;
|
||||
state.isDragging = true;
|
||||
state.startX = x;
|
||||
state.startY = y;
|
||||
state.hasMoved = false;
|
||||
card.style.transition = 'none';
|
||||
state.startX = e.clientX || e.touches[0].clientX;
|
||||
state.startY = e.clientY || e.touches[0].clientY;
|
||||
state.touchStartTime = Date.now();
|
||||
card.classList.add('swiping');
|
||||
};
|
||||
document.addEventListener('mousemove', onDragMove);
|
||||
document.addEventListener('touchmove', onDragMove, { passive: false });
|
||||
document.addEventListener('mouseup', onDragEnd);
|
||||
document.addEventListener('touchend', onDragEnd);
|
||||
}
|
||||
|
||||
const handlePointerMove = (x, y) => {
|
||||
function onDragMove(e) {
|
||||
if (!state.isDragging) return;
|
||||
e.preventDefault();
|
||||
state.hasMoved = true;
|
||||
|
||||
const currentX = e.clientX || e.touches[0].clientX;
|
||||
const currentY = e.clientY || e.touches[0].clientY;
|
||||
|
||||
state.moveX = currentX - state.startX;
|
||||
state.moveY = currentY - state.startY;
|
||||
|
||||
state.moveX = x - state.startX;
|
||||
state.moveY = y - state.startY;
|
||||
const rotation = state.moveX * 0.1;
|
||||
card.style.transform = `translate(${state.moveX}px, ${state.moveY}px) rotate(${rotation}deg)`;
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
function onDragEnd() {
|
||||
if (!state.isDragging) return;
|
||||
state.isDragging = false;
|
||||
card.classList.remove('swiping');
|
||||
|
||||
const absX = Math.abs(state.moveX);
|
||||
const absY = Math.abs(state.moveY);
|
||||
document.removeEventListener('mousemove', onDragMove);
|
||||
document.removeEventListener('touchmove', onDragMove);
|
||||
document.removeEventListener('mouseup', onDragEnd);
|
||||
document.removeEventListener('touchend', onDragEnd);
|
||||
|
||||
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');
|
||||
if (!state.hasMoved) return;
|
||||
|
||||
const rotation = state.moveX * 0.1;
|
||||
|
||||
if (Math.abs(rotation) > ACTION_ANGLE_THRESHOLD || Math.abs(state.moveX) > SWIPE_THRESHOLD || Math.abs(state.moveY) > SWIPE_THRESHOLD) {
|
||||
const angle = Math.atan2(state.moveY, state.moveX) * 180 / Math.PI;
|
||||
let direction;
|
||||
if (angle > -45 && angle <= 45) {
|
||||
direction = 'right';
|
||||
} else if (angle > 45 && angle <= 135) {
|
||||
direction = 'down';
|
||||
} else if (angle > 135 || angle <= -135) {
|
||||
direction = 'left';
|
||||
} else if (angle > -135 && angle <= -45) {
|
||||
direction = 'up';
|
||||
}
|
||||
performSwipe(direction);
|
||||
} else {
|
||||
card.style.transform = '';
|
||||
resetCardPosition();
|
||||
}
|
||||
|
||||
|
||||
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'));
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (state.isLoading || document.activeElement === searchInput) return;
|
||||
|
||||
const keyMap = {
|
||||
ArrowLeft: 'left',
|
||||
ArrowRight: 'right',
|
||||
ArrowUp: 'up',
|
||||
ArrowDown: 'down',
|
||||
};
|
||||
|
||||
if (keyMap[e.key]) {
|
||||
e.preventDefault(); // Prevent scrolling
|
||||
performSwipe(keyMap[e.key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filter buttons
|
||||
orientationFilters.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('button');
|
||||
if (!button) return;
|
||||
|
||||
const clickedOrientation = button.dataset.orientation;
|
||||
|
||||
if (clickedOrientation === 'all') {
|
||||
state.currentOrientation = ['all'];
|
||||
} else {
|
||||
// If 'all' was the only active filter, start a new selection
|
||||
if (state.currentOrientation.length === 1 && state.currentOrientation[0] === 'all') {
|
||||
state.currentOrientation = [];
|
||||
}
|
||||
|
||||
const index = state.currentOrientation.indexOf(clickedOrientation);
|
||||
if (index > -1) {
|
||||
// Already selected, so deselect
|
||||
state.currentOrientation.splice(index, 1);
|
||||
} else {
|
||||
// Not selected, so select
|
||||
state.currentOrientation.push(clickedOrientation);
|
||||
}
|
||||
}
|
||||
|
||||
// If no filters are selected after interaction, default to 'all'
|
||||
if (state.currentOrientation.length === 0) {
|
||||
state.currentOrientation = ['all'];
|
||||
}
|
||||
|
||||
// Update UI based on the state
|
||||
orientationFilters.querySelectorAll('button').forEach(btn => {
|
||||
if (state.currentOrientation.includes(btn.dataset.orientation)) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
state.currentOrientation = [button.dataset.value];
|
||||
loadNewImage();
|
||||
});
|
||||
|
||||
actionFilters.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('button');
|
||||
if (!button) return;
|
||||
|
||||
const clickedAction = button.dataset.action;
|
||||
|
||||
if (state.currentActions.length === 1 && state.currentActions[0] === 'Unactioned' && clickedAction !== 'Unactioned') {
|
||||
state.currentActions = [];
|
||||
}
|
||||
|
||||
const index = state.currentActions.indexOf(clickedAction);
|
||||
if (index > -1) {
|
||||
state.currentActions.splice(index, 1);
|
||||
} else {
|
||||
state.currentActions.push(clickedAction);
|
||||
}
|
||||
|
||||
if (state.currentActions.length === 0) {
|
||||
state.currentActions = ['Unactioned'];
|
||||
}
|
||||
|
||||
actionFilters.querySelectorAll('button').forEach(btn => {
|
||||
if (state.currentActions.includes(btn.dataset.action)) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
state.currentActions = [button.dataset.value];
|
||||
loadNewImage();
|
||||
});
|
||||
|
||||
// Modal
|
||||
card.addEventListener('click', (e) => {
|
||||
if (!state.hasMoved && state.currentImageInfo) {
|
||||
fullscreenImage.src = card.querySelector('img').src;
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
closeModal.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Search
|
||||
const addKeyword = () => {
|
||||
const keyword = searchInput.value.trim();
|
||||
if (keyword && !state.searchKeywords.includes(keyword)) {
|
||||
state.searchKeywords.push(keyword);
|
||||
renderKeywordPills();
|
||||
searchInput.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeKeyword = (keywordToRemove) => {
|
||||
state.searchKeywords = state.searchKeywords.filter(k => k !== keywordToRemove);
|
||||
renderKeywordPills();
|
||||
};
|
||||
|
||||
const renderKeywordPills = () => {
|
||||
keywordPillsContainer.innerHTML = '';
|
||||
state.searchKeywords.forEach(keyword => {
|
||||
const pill = document.createElement('div');
|
||||
pill.className = 'keyword-pill';
|
||||
pill.textContent = keyword;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'remove-keyword';
|
||||
removeBtn.innerHTML = '×';
|
||||
removeBtn.dataset.keyword = keyword;
|
||||
const removeBtn = document.createElement('span');
|
||||
removeBtn.textContent = 'x';
|
||||
removeBtn.onclick = () => removeKeyword(keyword);
|
||||
pill.appendChild(removeBtn);
|
||||
|
||||
keywordPillsContainer.appendChild(pill);
|
||||
});
|
||||
};
|
||||
|
||||
const addSearchKeyword = () => {
|
||||
const newKeyword = searchInput.value.trim();
|
||||
if (newKeyword && !state.searchKeywords.includes(newKeyword)) {
|
||||
state.searchKeywords.push(newKeyword);
|
||||
renderKeywordPills();
|
||||
loadNewImage();
|
||||
}
|
||||
searchInput.value = '';
|
||||
searchInput.focus();
|
||||
};
|
||||
|
||||
searchButton.addEventListener('click', addSearchKeyword);
|
||||
|
||||
searchInput.addEventListener('keypress', (e) => {
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addSearchKeyword();
|
||||
addKeyword();
|
||||
}
|
||||
});
|
||||
|
||||
keywordPillsContainer.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('remove-keyword')) {
|
||||
const keywordToRemove = e.target.dataset.keyword;
|
||||
state.searchKeywords = state.searchKeywords.filter(k => k !== keywordToRemove);
|
||||
renderKeywordPills();
|
||||
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'}`;
|
||||
document.getElementById('modal-prompt-data').textContent = `Prompt: ${state.currentImageInfo.prompt_data || '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';
|
||||
}
|
||||
searchButton.addEventListener('click', () => {
|
||||
loadNewImage();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (searchInput === document.activeElement) return;
|
||||
|
||||
if (modal.style.display === 'flex' && e.key === 'Escape') {
|
||||
modal.style.display = 'none';
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user