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 = '
Error loading image.
'; }); }; 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 });