405 lines
15 KiB
JavaScript
405 lines
15 KiB
JavaScript
import { showToast, updateImageInfo } from './utils.js';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const state = {
|
|
currentImageInfo: null,
|
|
currentOrientation: ['all'],
|
|
currentActions: ['Unactioned'],
|
|
previousOrientation: ['all'],
|
|
allowNsfw: false,
|
|
searchKeywords: [],
|
|
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 actionFilters = document.querySelector('.action-filters');
|
|
const modal = document.getElementById('fullscreen-modal');
|
|
const fullscreenImage = document.getElementById('fullscreen-image');
|
|
const closeModal = document.querySelector('.close-modal');
|
|
const searchInput = document.getElementById('search-input');
|
|
const nsfwToggleBtn = document.getElementById('toggle-nsfw');
|
|
const searchButton = document.getElementById('search-button');
|
|
const keywordPillsContainer = document.getElementById('keyword-pills-container');
|
|
|
|
const SWIPE_THRESHOLD = 100;
|
|
|
|
const performSwipe = (direction) => {
|
|
if (!state.currentImageInfo) return;
|
|
|
|
card.classList.add(`swipe-${direction}`);
|
|
const actionNameMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' };
|
|
const actionName = actionNameMap[direction] || direction;
|
|
lastActionText.textContent = `Last action: ${actionName}`;
|
|
const toastMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' };
|
|
showToast(toastMap[direction] || 'Action');
|
|
|
|
recordSelection(state.currentImageInfo, actionName);
|
|
|
|
setTimeout(() => {
|
|
card.classList.remove(`swipe-${direction}`);
|
|
loadNewImage();
|
|
}, 500);
|
|
};
|
|
|
|
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({
|
|
orientation: state.currentOrientation.join(','),
|
|
t: new Date().getTime(),
|
|
});
|
|
|
|
// NSFW param
|
|
params.append('allow_nsfw', state.allowNsfw ? '1' : '0');
|
|
|
|
if (state.searchKeywords.length > 0) {
|
|
params.append('search', state.searchKeywords.join(','));
|
|
}
|
|
|
|
if (state.currentActions.length > 0) {
|
|
params.append('actions', state.currentActions.join(','));
|
|
}
|
|
|
|
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>';
|
|
});
|
|
};
|
|
|
|
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'));
|
|
|
|
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]);
|
|
}
|
|
});
|
|
|
|
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');
|
|
}
|
|
});
|
|
|
|
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');
|
|
}
|
|
});
|
|
|
|
loadNewImage();
|
|
});
|
|
|
|
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;
|
|
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) => {
|
|
if (e.key === 'Enter') {
|
|
addSearchKeyword();
|
|
}
|
|
});
|
|
|
|
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';
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
|
|
// --- Ultra-wide mode ---
|
|
const fullscreenToggle = document.getElementById('fullscreen-toggle');
|
|
fullscreenToggle.setAttribute('title', 'Toggle fullscreen Mode');
|
|
|
|
const setfullscreenMode = (isActive) => {
|
|
if (isActive) {
|
|
// Entering ultra-wide mode: just disable filter controls
|
|
orientationFilters.style.pointerEvents = 'none';
|
|
orientationFilters.style.opacity = '0.5';
|
|
} else {
|
|
// Exiting ultra-wide mode: re-enable filter controls
|
|
orientationFilters.style.pointerEvents = 'auto';
|
|
orientationFilters.style.opacity = '1';
|
|
}
|
|
};
|
|
|
|
fullscreenToggle.addEventListener('click', () => {
|
|
const isActive = document.body.classList.toggle('fullscreen-mode');
|
|
localStorage.setItem('fullscreenMode', isActive);
|
|
showToast(isActive ? 'fullscreen mode enabled' : 'fullscreen mode disabled');
|
|
setfullscreenMode(isActive);
|
|
});
|
|
|
|
// Check for saved preference on load
|
|
const isfullscreenModeOnLoad = localStorage.getItem('fullscreenMode') === 'true';
|
|
if (isfullscreenModeOnLoad) {
|
|
document.body.classList.add('fullscreen-mode');
|
|
setfullscreenMode(true);
|
|
}
|
|
|
|
// --- NSFW toggle ---
|
|
nsfwToggleBtn.addEventListener('click', () => {
|
|
state.allowNsfw = !state.allowNsfw;
|
|
nsfwToggleBtn.dataset.allow = state.allowNsfw ? '1' : '0';
|
|
nsfwToggleBtn.classList.toggle('active', state.allowNsfw);
|
|
loadNewImage();
|
|
});
|
|
|
|
loadNewImage(); // Always load an image on startup
|
|
});
|