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, isAnimating: false, // Flag to prevent actions during animation }; 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 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 || state.isAnimating) return; state.isAnimating = true; 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); // 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(); state.isAnimating = false; }, { once: true }); }; async function getNextImage() { const queryParams = new URLSearchParams({ orientation: state.currentOrientation.join(','), actions: state.currentActions.join(','), nsfw: state.allowNsfw, keywords: state.searchKeywords.join(','), }).toString(); const response = await fetch(`/next-image?${queryParams}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } 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)); } 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'); } 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 { handleNoImageAvailable(); } } catch (error) { console.error('Error loading image:', error); showToast('Failed to load image.', 'error'); handleNoImageAvailable(); } finally { state.isLoading = false; loadingIndicator.style.display = 'none'; } } // --- 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.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(); document.addEventListener('mousemove', onDragMove); document.addEventListener('touchmove', onDragMove, { passive: false }); document.addEventListener('mouseup', onDragEnd); document.addEventListener('touchend', onDragEnd); } 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; const rotation = state.moveX * 0.1; card.style.transform = `translate(${state.moveX}px, ${state.moveY}px) rotate(${rotation}deg)`; } function onDragEnd() { if (!state.isDragging) return; state.isDragging = false; document.removeEventListener('mousemove', onDragMove); document.removeEventListener('touchmove', onDragMove); document.removeEventListener('mouseup', onDragEnd); document.removeEventListener('touchend', onDragEnd); 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 { resetCardPosition(); } state.moveX = 0; state.moveY = 0; } // Filter buttons orientationFilters.addEventListener('click', (e) => { const button = e.target.closest('button'); if (!button) return; state.currentOrientation = [button.dataset.value]; loadNewImage(); }); actionFilters.addEventListener('click', (e) => { const button = e.target.closest('button'); if (!button) return; 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('span'); removeBtn.textContent = 'x'; removeBtn.onclick = () => removeKeyword(keyword); pill.appendChild(removeBtn); keywordPillsContainer.appendChild(pill); }); }; searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { addKeyword(); } }); 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; } 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 });