/** * Enhanced Main Application with Physics-Based Swipe Animations * Integrates SwipePhysics engine and EnhancedSwipeCard component */ import { showToast, updateImageInfo } from './utils.js'; import { swipeIntegration, createSwipeSettingsPanel } from './swipe-integration.js'; document.addEventListener('DOMContentLoaded', () => { // Enhanced application state const state = { currentImageInfo: null, currentOrientation: ['all'], currentActions: ['Unactioned'], previousOrientation: ['all'], allowNsfw: false, searchKeywords: [], sortOrder: 'random', isLoading: false, // Physics and animation state animationQuality: getAnimationQuality(), performanceMode: getPerformanceMode(), reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, // Statistics for performance monitoring stats: { totalSwipes: 0, averageSwipeTime: 0, lastSwipeTime: 0, performanceMetrics: [] } }; // DOM elements 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 fullscreenToggle = document.getElementById('fullscreen-toggle'); // Initialize swipe system using integration manager let enhancedSwipeCard = null; // Initialize the swipe integration system swipeIntegration.initialize( document.querySelector('.swipe-container'), performEnhancedSwipe, { threshold: 120, velocityThreshold: state.reducedMotion ? 1 : 3, springTension: state.animationQuality === 'high' ? 320 : 280, springFriction: state.animationQuality === 'high' ? 28 : 35, mass: 1.1, momentumDecay: 0.91, enablePhysics: true, enableVisualEffects: true, enableHapticFeedback: true, animationQuality: state.animationQuality, performanceMode: state.performanceMode } ).then(() => { enhancedSwipeCard = swipeIntegration.getSwipeCard(); console.log('Swipe system initialized successfully'); // Setup modal interactions after swipe card is initialized if (enhancedSwipeCard && enhancedSwipeCard.card) { enhancedSwipeCard.card.addEventListener('click', () => { if (!enhancedSwipeCard.state.hasMoved && state.currentImageInfo) { openEnhancedModal(); } }); } // Load initial image after initialization loadNewImageWithAnimation(); }).catch(error => { console.error('Failed to initialize swipe system:', error); // Fallback to basic functionality loadNewImageWithAnimation(); }); // Performance monitoring let performanceObserver; if ('PerformanceObserver' in window) { performanceObserver = new PerformanceObserver((list) => { const entries = list.getEntries(); entries.forEach(entry => { if (entry.entryType === 'measure' && entry.name.includes('swipe')) { state.stats.performanceMetrics.push({ name: entry.name, duration: entry.duration, timestamp: entry.startTime }); // Keep only last 50 measurements if (state.stats.performanceMetrics.length > 50) { state.stats.performanceMetrics.shift(); } } }); }); performanceObserver.observe({ entryTypes: ['measure'] }); } /** * Enhanced swipe handler with physics feedback */ function performEnhancedSwipe(direction) { if (!state.currentImageInfo) return; // Prevent duplicate calls within a short time window const now = performance.now(); if (state.stats.lastSwipeTime && (now - state.stats.lastSwipeTime) < 500) { console.log('Duplicate swipe prevented:', direction); return; } const swipeStartTime = now; performance.mark('swipe-start'); // Update statistics state.stats.totalSwipes++; state.stats.lastSwipeTime = swipeStartTime; // Enhanced action mapping with more descriptive feedback const actionMap = { left: 'Discarded', right: 'Kept', up: 'Favorited', down: 'Marked for review' }; // Backend action mapping (single letters for simplicity and backwards compatibility) const backendActionMap = { left: 'D', // Discard right: 'K', // Keep up: 'F', // Favourite down: 'R' // Review }; const actionName = actionMap[direction] || direction; const backendAction = backendActionMap[direction] || direction; lastActionText.textContent = `Last action: ${actionName}`; // Enhanced toast with physics-based animation showEnhancedToast(actionName, direction); // Record selection with performance tracking - use backend action name recordEnhancedSelection(state.currentImageInfo, backendAction); // Add haptic feedback for supported devices if ('vibrate' in navigator && !state.reducedMotion) { const vibrationPattern = getVibrationPattern(direction); navigator.vibrate(vibrationPattern); } // Performance measurement performance.mark('swipe-end'); performance.measure('swipe-complete', 'swipe-start', 'swipe-end'); // Update average swipe time const swipeTime = performance.now() - swipeStartTime; state.stats.averageSwipeTime = ( (state.stats.averageSwipeTime * (state.stats.totalSwipes - 1) + swipeTime) / state.stats.totalSwipes ); // Load new image with staggered animation setTimeout(() => { loadNewImageWithAnimation(); }, state.animationQuality === 'high' ? 600 : 400); } /** * Enhanced toast notification with direction-based styling */ function showEnhancedToast(message, direction) { const toastEl = document.getElementById('toast'); if (!toastEl) return; // Add direction-specific class for styling toastEl.className = `toast enhanced-toast toast-${direction}`; // Enhanced message with icon const icons = { left: '🗑️', right: '✅', up: '⭐', down: '⏰' }; toastEl.innerHTML = ` ${icons[direction] || '📱'} ${message} `; toastEl.classList.add('show'); // Auto-hide with enhanced timing setTimeout(() => { toastEl.classList.remove('show'); setTimeout(() => { toastEl.className = 'toast'; }, 300); }, state.reducedMotion ? 2000 : 3000); } /** * Get vibration pattern based on swipe direction */ function getVibrationPattern(direction) { const patterns = { left: [50, 30, 50], // Double tap for discard right: [100], // Single strong for keep up: [30, 20, 30, 20, 30], // Triple tap for favorite down: [80, 40, 80] // Double long for review }; return patterns[direction] || [50]; } /** * Enhanced selection recording with retry logic */ async function recordEnhancedSelection(imageInfo, action) { const maxRetries = 3; let retryCount = 0; const attemptRecord = async () => { try { const response = await fetch('/selection', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Request-ID': generateRequestId() }, body: JSON.stringify({ image_path: imageInfo.path, action, timestamp: Date.now(), swipe_stats: { total_swipes: state.stats.totalSwipes, average_time: state.stats.averageSwipeTime } }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); console.log('Enhanced selection recorded:', data); return data; } catch (error) { console.error(`Selection recording attempt ${retryCount + 1} failed:`, error); if (retryCount < maxRetries) { retryCount++; // Exponential backoff const delay = Math.pow(2, retryCount) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); return attemptRecord(); } else { // Show user-friendly error showEnhancedToast('Failed to save selection', 'error'); throw error; } } }; return attemptRecord(); } /** * Load new image with enhanced animations */ function loadNewImageWithAnimation() { if (state.isLoading) return; state.isLoading = true; performance.mark('image-load-start'); // Show enhanced loading state if (enhancedSwipeCard && enhancedSwipeCard.showLoading) { enhancedSwipeCard.showLoading(); } const params = new URLSearchParams({ orientation: state.currentOrientation.join(','), t: new Date().getTime(), allow_nsfw: state.allowNsfw ? '1' : '0', sort: state.sortOrder }); 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 => { if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return response.json(); }) .then(data => { state.isLoading = false; if (enhancedSwipeCard && enhancedSwipeCard.hideLoading) { enhancedSwipeCard.hideLoading(); } performance.mark('image-load-end'); performance.measure('image-load-complete', 'image-load-start', 'image-load-end'); if (data && data.path) { state.currentImageInfo = data; // Set image with enhanced loading animation if (enhancedSwipeCard && enhancedSwipeCard.setImage) { enhancedSwipeCard.setImage(data); } updateImageInfo(data); adjustContainerToImageWithAnimation(data.orientation); // Preload next image for smoother experience if (state.performanceMode !== 'low') { preloadNextImage(); } } else { handleNoImagesState(data); } }) .catch(error => { console.error('Enhanced image loading error:', error); state.isLoading = false; if (enhancedSwipeCard && enhancedSwipeCard.hideLoading) { enhancedSwipeCard.hideLoading(); } // Show enhanced error state showEnhancedError('Failed to load image. Please try again.'); }); } /** * Adjust container with smooth animation */ function adjustContainerToImageWithAnimation(orientation) { const container = document.querySelector('.swipe-container'); if (window.innerWidth < 992) return; // Skip on mobile container.style.transition = 'all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; if (orientation === 'landscape') { container.style.flex = '4'; container.style.transform = 'scale(1.02)'; } else { container.style.flex = '2'; container.style.transform = 'scale(1)'; } // Reset transform after animation setTimeout(() => { container.style.transform = ''; }, 600); } /** * Preload next image for smoother experience */ function preloadNextImage() { const params = new URLSearchParams({ orientation: state.currentOrientation.join(','), allow_nsfw: state.allowNsfw ? '1' : '0', sort: state.sortOrder, preload: '1' }); fetch(`/random-image?${params.toString()}`) .then(response => response.json()) .then(data => { if (data && data.path) { // Preload the image const img = new Image(); img.src = data.path; } }) .catch(() => { // Silently fail preloading }); } /** * Handle no images state with enhanced feedback */ function handleNoImagesState(data) { const message = data?.message || 'No more images available.'; if (enhancedSwipeCard && enhancedSwipeCard.card) { enhancedSwipeCard.card.innerHTML = `
📷
${message}
`; } else { // Fallback for when swipe card isn't initialized const container = document.querySelector('.swipe-container .image-card'); if (container) { container.innerHTML = `
📷
${message}
`; } } updateImageInfo({ filename: 'No image', creation_date: '', resolution: '', prompt_data: '' }); state.currentImageInfo = null; } /** * Show enhanced error message */ function showEnhancedError(message) { if (enhancedSwipeCard && enhancedSwipeCard.card) { enhancedSwipeCard.card.innerHTML = `
⚠️
${message}
`; } else { // Fallback for when swipe card isn't initialized const container = document.querySelector('.swipe-container .image-card'); if (container) { container.innerHTML = `
⚠️
${message}
`; } } } // Enhanced event listeners with improved performance setupEnhancedEventListeners(); setupKeyboardShortcuts(); setupPerformanceMonitoring(); /** * Setup enhanced event listeners */ function setupEnhancedEventListeners() { // Orientation filters with enhanced feedback orientationFilters?.addEventListener('click', (e) => { const button = e.target.closest('button'); if (!button) return; // Add ripple effect createRippleEffect(button, e); const clickedOrientation = button.dataset.orientation; if (clickedOrientation === 'all') { state.currentOrientation = ['all']; } else { if (state.currentOrientation.length === 1 && state.currentOrientation[0] === 'all') { state.currentOrientation = []; } const index = state.currentOrientation.indexOf(clickedOrientation); if (index > -1) { state.currentOrientation.splice(index, 1); } else { state.currentOrientation.push(clickedOrientation); } } if (state.currentOrientation.length === 0) { state.currentOrientation = ['all']; } updateFilterButtonStates(orientationFilters, state.currentOrientation); loadNewImageWithAnimation(); }); // Action filters with enhanced feedback actionFilters?.addEventListener('click', (e) => { const button = e.target.closest('button'); if (!button) return; createRippleEffect(button, e); 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']; } updateFilterButtonStates(actionFilters, state.currentActions); loadNewImageWithAnimation(); }); // Enhanced search functionality searchButton?.addEventListener('click', addSearchKeywordWithAnimation); searchInput?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { addSearchKeywordWithAnimation(); } }); closeModal?.addEventListener('click', closeEnhancedModal); modal?.addEventListener('click', (e) => { if (e.target === modal) { closeEnhancedModal(); } }); // NSFW toggle with enhanced feedback nsfwToggleBtn?.addEventListener('click', () => { state.allowNsfw = !state.allowNsfw; nsfwToggleBtn.dataset.allow = state.allowNsfw ? '1' : '0'; nsfwToggleBtn.classList.toggle('active', state.allowNsfw); // Enhanced visual feedback createButtonPulse(nsfwToggleBtn); loadNewImageWithAnimation(); }); // Sort order change with enhanced feedback document.getElementById('sort-order')?.addEventListener('change', (e) => { state.sortOrder = e.target.value; showEnhancedToast(`Sorted by: ${e.target.selectedOptions[0].text}`, 'info'); loadNewImageWithAnimation(); }); // Button click handlers with enhanced feedback document.getElementById('btn-left')?.addEventListener('click', () => { createButtonPulse(document.getElementById('btn-left')); performEnhancedSwipe('left'); }); document.getElementById('btn-right')?.addEventListener('click', () => { createButtonPulse(document.getElementById('btn-right')); performEnhancedSwipe('right'); }); document.getElementById('btn-up')?.addEventListener('click', () => { createButtonPulse(document.getElementById('btn-up')); performEnhancedSwipe('up'); }); document.getElementById('btn-down')?.addEventListener('click', () => { createButtonPulse(document.getElementById('btn-down')); performEnhancedSwipe('down'); }); // Fullscreen toggle functionality fullscreenToggle?.addEventListener('click', () => { toggleFullscreenMode(); }); } /** * Setup keyboard shortcuts with enhanced feedback */ function setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (state.isLoading || document.activeElement === searchInput) return; if (modal?.style.display === 'flex') return; const keyMap = { ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down', KeyA: 'left', KeyD: 'right', KeyW: 'up', KeyS: 'down' }; if (keyMap[e.code]) { e.preventDefault(); // Visual feedback for keyboard shortcuts const button = document.getElementById(`btn-${keyMap[e.code]}`); if (button) { createButtonPulse(button); } performEnhancedSwipe(keyMap[e.code]); } // Modal controls if (modal?.style.display === 'flex' && e.key === 'Escape') { closeEnhancedModal(); } // Fullscreen controls if (e.key === 'Escape' && document.body.classList.contains('fullscreen-mode')) { e.preventDefault(); toggleFullscreenMode(); } // F11 alternative for fullscreen (F key) if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggleFullscreenMode(); } }); } /** * Setup performance monitoring */ function setupPerformanceMonitoring() { // Monitor frame rate let frameCount = 0; let lastTime = performance.now(); function monitorFPS() { frameCount++; const currentTime = performance.now(); if (currentTime - lastTime >= 1000) { const fps = Math.round((frameCount * 1000) / (currentTime - lastTime)); // Adjust animation quality based on performance if (fps < 30 && state.animationQuality === 'high') { state.animationQuality = 'medium'; console.log('Reduced animation quality due to low FPS:', fps); } else if (fps > 50 && state.animationQuality === 'medium') { state.animationQuality = 'high'; console.log('Increased animation quality due to good FPS:', fps); } frameCount = 0; lastTime = currentTime; } requestAnimationFrame(monitorFPS); } if (state.performanceMode !== 'low') { requestAnimationFrame(monitorFPS); } } // Utility functions function getAnimationQuality() { const userAgent = navigator.userAgent.toLowerCase(); const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/.test(userAgent); const isLowEnd = /android.*chrome\/[1-6][0-9]/.test(userAgent); if (isLowEnd) return 'low'; if (isMobile) return 'medium'; return 'high'; } function getPerformanceMode() { if ('connection' in navigator) { const connection = navigator.connection; if (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g') { return 'low'; } } return 'normal'; } function generateRequestId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } function createRippleEffect(button, event) { if (state.reducedMotion) return; const ripple = document.createElement('span'); ripple.classList.add('ripple-effect'); const rect = button.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = event.clientX - rect.left - size / 2; const y = event.clientY - rect.top - size / 2; ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; button.appendChild(ripple); setTimeout(() => { ripple.remove(); }, 600); } function createButtonPulse(button) { if (state.reducedMotion) return; button.style.transform = 'scale(0.95)'; setTimeout(() => { button.style.transform = ''; }, 150); } function updateFilterButtonStates(container, activeStates) { container.querySelectorAll('button').forEach(btn => { const isActive = activeStates.includes(btn.dataset.orientation || btn.dataset.action); btn.classList.toggle('active', isActive); }); } function addSearchKeywordWithAnimation() { const newKeyword = searchInput.value.trim(); if (newKeyword && !state.searchKeywords.includes(newKeyword)) { state.searchKeywords.push(newKeyword); renderKeywordPillsWithAnimation(); loadNewImageWithAnimation(); } searchInput.value = ''; searchInput.focus(); } function renderKeywordPillsWithAnimation() { keywordPillsContainer.innerHTML = ''; state.searchKeywords.forEach((keyword, index) => { const pill = document.createElement('div'); pill.className = 'keyword-pill enhanced-pill'; pill.style.animationDelay = `${index * 100}ms`; pill.innerHTML = ` ${keyword} `; keywordPillsContainer.appendChild(pill); }); // Add event listeners for removal 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); renderKeywordPillsWithAnimation(); loadNewImageWithAnimation(); } }); } function openEnhancedModal() { if (!state.currentImageInfo) return; 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'; if (!state.reducedMotion) { setTimeout(() => { modal.classList.add('show'); }, 10); } else { modal.classList.add('show'); } } function closeEnhancedModal() { modal.classList.remove('show'); setTimeout(() => { modal.style.display = 'none'; }, state.reducedMotion ? 0 : 400); } // Setup settings panel setupSettingsPanel(); // Expose state for debugging window.enhancedSwipeState = state; window.enhancedSwipeCard = enhancedSwipeCard; window.swipeIntegration = swipeIntegration; /** * Setup settings panel for swipe mode selection */ function setupSettingsPanel() { const settingsToggle = document.getElementById('swipe-settings-toggle'); const settingsPanelContainer = document.getElementById('swipe-settings-panel'); let settingsPanel = null; let isPanelOpen = false; if (!settingsToggle || !settingsPanelContainer) { console.warn('Settings panel elements not found'); return; } settingsToggle.addEventListener('click', () => { if (!isPanelOpen) { // Create and show settings panel settingsPanel = createSwipeSettingsPanel(swipeIntegration); settingsPanelContainer.appendChild(settingsPanel); settingsPanelContainer.style.display = 'block'; // Add show class after a brief delay for animation setTimeout(() => { settingsPanel.classList.add('show'); }, 10); isPanelOpen = true; // Add click outside to close document.addEventListener('click', handleOutsideClick); } else { closeSettingsPanel(); } }); function handleOutsideClick(e) { if (settingsPanel && !settingsPanel.contains(e.target) && !settingsToggle.contains(e.target)) { closeSettingsPanel(); } } function closeSettingsPanel() { if (settingsPanel) { settingsPanel.classList.remove('show'); setTimeout(() => { settingsPanelContainer.removeChild(settingsPanel); settingsPanelContainer.style.display = 'none'; settingsPanel = null; }, 300); } isPanelOpen = false; document.removeEventListener('click', handleOutsideClick); } // Add keyboard shortcut for settings (Ctrl/Cmd + ,) document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === ',') { e.preventDefault(); settingsToggle.click(); } }); } /** * Toggle fullscreen mode for immersive image viewing */ function toggleFullscreenMode() { const body = document.body; const isFullscreen = body.classList.contains('fullscreen-mode'); if (isFullscreen) { // Exit fullscreen mode body.classList.remove('fullscreen-mode'); fullscreenToggle.setAttribute('aria-label', 'Enter fullscreen mode'); fullscreenToggle.title = 'Enter fullscreen mode'; // Enhanced feedback showEnhancedToast('Exited fullscreen mode', 'info'); } else { // Enter fullscreen mode body.classList.add('fullscreen-mode'); fullscreenToggle.setAttribute('aria-label', 'Exit fullscreen mode'); fullscreenToggle.title = 'Exit fullscreen mode'; // Enhanced feedback showEnhancedToast('Entered fullscreen mode', 'info'); } // Add visual feedback to the button createButtonPulse(fullscreenToggle); // Trigger a resize event to help any responsive components adjust setTimeout(() => { window.dispatchEvent(new Event('resize')); }, 300); } });