document.addEventListener('DOMContentLoaded', function() { const card = document.getElementById('current-card'); const lastActionText = document.getElementById('last-action'); const leftHint = document.querySelector('.left-hint'); const rightHint = document.querySelector('.right-hint'); const upHint = document.querySelector('.up-hint'); const downHint = document.querySelector('.down-hint'); const swipeContainer = document.querySelector('.swipe-container'); console.log('DOM Content Loaded - initializing app'); // Image cache for preloading const imageCache = { images: [], // Will store objects with {path, data, element} maxSize: 2, // Cache up to 2 images // Add an image to the cache add: function(imageData) { // Create a new Image element for preloading const img = new Image(); img.src = imageData.path; // Add to the cache this.images.push({ path: imageData.path, data: imageData, element: img }); console.log(`Added image to cache: ${imageData.path}`); // Trim cache if it exceeds max size if (this.images.length > this.maxSize) { this.images.shift(); // Remove oldest image } }, // Get an image from cache if available get: function(path) { const cachedImage = this.images.find(img => img.path === path); if (cachedImage) { console.log(`Cache hit for: ${path}`); // Remove this image from the cache since we're using it this.images = this.images.filter(img => img.path !== path); return cachedImage; } console.log(`Cache miss for: ${path}`); return null; } }; // Detect if we're on a mobile device const isMobile = window.matchMedia("(max-width: 768px)").matches; console.log('Mobile view detection:', isMobile ? 'mobile' : 'desktop'); // Apply mobile-specific behaviors if (isMobile) { console.log('Applying mobile-specific behaviors'); // Show swipe hints briefly on page load to educate users setTimeout(() => { showAllHints(); setTimeout(hideAllHints, 3000); }, 1000); } // Important: Load the first image regardless of viewport size console.log('Triggering initial image load'); // Wait a short moment to ensure all initialization is complete setTimeout(() => { loadNewImage(); }, 300); // Adjust swipe container height to fill available space function adjustSwipeContainerHeight() { const viewportHeight = window.innerHeight; const containerTop = swipeContainer.getBoundingClientRect().top; const statusAreaHeight = document.querySelector('.status-area').offsetHeight; const actionButtonsHeight = document.querySelector('.action-buttons') ? document.querySelector('.action-buttons').offsetHeight : 0; const footerSpace = 20; // Extra space for padding/margin // Calculate available height const availableHeight = viewportHeight - containerTop - statusAreaHeight - actionButtonsHeight - footerSpace; // Set minimum height to ensure it's usable const minHeight = isMobile ? '60vh' : '400px'; swipeContainer.style.minHeight = `max(${availableHeight}px, ${minHeight})`; console.log('Adjusted container height: ', swipeContainer.style.minHeight); } // Run on load and on resize adjustSwipeContainerHeight(); window.addEventListener('resize', adjustSwipeContainerHeight); // Make sure the first image loads after layout calculations are done window.addEventListener('load', function() { console.log('Window fully loaded - requesting first image'); loadNewImage(); }); // Add the animation class to the initial card setTimeout(() => { card.classList.add('new-card'); }, 100); // Small delay to ensure DOM is ready // Modal elements const modal = document.getElementById('fullscreen-modal'); const fullscreenImage = document.getElementById('fullscreen-image'); const closeModal = document.querySelector('.close-modal'); const modalResolution = document.getElementById('modal-resolution'); const modalFilename = document.getElementById('modal-filename'); const modalCreationDate = document.getElementById('modal-creation-date'); // Current image information let currentImageInfo = null; // Button event listeners 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')); // Touch start time for distinguishing between swipe and tap let touchStartTime = 0; // Touch variables let startX, startY, moveX, moveY; let isDragging = false; const swipeThreshold = 150; // Increased minimum distance for a swipe to be registered let hasMoved = false; // Track if significant movement occurred // Touch event handlers with passive: false for better mobile performance card.addEventListener('touchstart', handleTouchStart, { passive: false }); card.addEventListener('touchmove', handleTouchMove, { passive: false }); card.addEventListener('touchend', handleTouchEnd, { passive: false }); // Mouse event handlers (for desktop testing) card.addEventListener('mousedown', handleMouseDown, false); document.addEventListener('mousemove', handleMouseMove, false); document.addEventListener('mouseup', handleMouseUp, false); // Click handler for viewing full-resolution image card.addEventListener('click', handleCardClick); // Close modal when clicking the close button closeModal.addEventListener('click', () => { modal.style.display = 'none'; }); // Close modal when clicking outside the image window.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; } }); // Close modal with escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && modal.style.display === 'block') { modal.style.display = 'none'; } }); function handleTouchStart(e) { // Store the initial position and set the dragging flag const touch = e.touches[0]; startX = touch.clientX; startY = touch.clientY; isDragging = true; hasMoved = false; // Reset movement tracking card.classList.add('swiping'); // Record touch start time to distinguish between tap and swipe touchStartTime = new Date().getTime(); // Prevent default to avoid scrolling while swiping e.preventDefault(); // Check if we're on mobile const isMobile = window.matchMedia("(max-width: 768px)").matches; // Show swipe hints on mobile if (isMobile) { showAllHints(); } } function handleTouchMove(e) { if (!isDragging) return; const touch = e.touches[0]; moveX = touch.clientX - startX; moveY = touch.clientY - startY; // Check if we've moved significantly const absX = Math.abs(moveX); const absY = Math.abs(moveY); if (Math.max(absX, absY) > 20) { hasMoved = true; } // Calculate a fade factor based on distance (further = more transparent) const maxDistance = Math.max(window.innerWidth, window.innerHeight) * 0.4; const distance = Math.sqrt(moveX * moveX + moveY * moveY); const fadeAmount = Math.min(0.7, distance / maxDistance); // Apply transform with reduced rotation and opacity based on swipe distance card.style.transform = `translate(${moveX}px, ${moveY}px) rotate(${moveX * 0.02}deg)`; card.style.opacity = `${1 - fadeAmount}`; // Show appropriate hint based on swipe direction updateHints(moveX, moveY); // Add visual feedback based on swipe direction updateVisualFeedback(moveX, moveY); // Prevent default to avoid scrolling while swiping e.preventDefault(); } // Add visual feedback based on swipe direction function updateVisualFeedback(moveX, moveY) { // Reset all borders card.style.boxShadow = '0 10px 20px rgba(0, 0, 0, 0.2)'; const absX = Math.abs(moveX); const absY = Math.abs(moveY); // Only show feedback if we've moved enough if (Math.max(absX, absY) < swipeThreshold / 2) return; if (absX > absY) { // Horizontal swipe if (moveX > 0) { // Right swipe - green glow card.style.boxShadow = '0 0 20px 5px rgba(46, 213, 115, 0.7)'; } else { // Left swipe - red glow card.style.boxShadow = '0 0 20px 5px rgba(255, 71, 87, 0.7)'; } } else { // Vertical swipe if (moveY > 0) { // Down swipe - yellow glow card.style.boxShadow = '0 0 20px 5px rgba(255, 165, 2, 0.7)'; } else { // Up swipe - blue glow card.style.boxShadow = '0 0 20px 5px rgba(30, 144, 255, 0.7)'; } } } function handleTouchEnd(e) { if (!isDragging) return; // Calculate touch duration const touchEndTime = new Date().getTime(); const touchDuration = touchEndTime - touchStartTime; // Determine if this was a tap (short touch with minimal movement) const absX = Math.abs(moveX || 0); const absY = Math.abs(moveY || 0); // More generous tap detection - increased movement threshold to 30px const isTap = touchDuration < 300 && Math.max(absX, absY) < 30; isDragging = false; if (isTap || !hasMoved) { // This was a tap or minimal movement, not a swipe resetCardPosition(); handleCardClick(e); } else if (Math.max(absX, absY) > swipeThreshold && touchDuration > 100) { // This was a swipe if (absX > absY) { // Horizontal swipe if (moveX > 0) { performSwipe('right'); } else { performSwipe('left'); } } else { // Vertical swipe if (moveY > 0) { performSwipe('down'); } else { performSwipe('up'); } } } else { // Reset card position if swipe wasn't strong enough resetCardPosition(); } // Hide all hints hideAllHints(); } function handleMouseDown(e) { // Store the initial position and set the dragging flag startX = e.clientX; startY = e.clientY; isDragging = true; card.classList.add('swiping'); // Prevent default to avoid text selection during drag e.preventDefault(); } function handleMouseMove(e) { if (!isDragging) return; moveX = e.clientX - startX; moveY = e.clientY - startY; // Apply transform to the card card.style.transform = `translate(${moveX}px, ${moveY}px) rotate(${moveX * 0.1}deg)`; // Show appropriate hint based on direction updateHints(moveX, moveY); } function handleMouseUp(e) { if (!isDragging) return; // Determine if this was a click (minimal movement) or a swipe const absX = Math.abs(moveX || 0); const absY = Math.abs(moveY || 0); isDragging = false; if (Math.max(absX, absY) > swipeThreshold) { if (absX > absY) { // Horizontal swipe if (moveX > 0) { performSwipe('right'); } else { performSwipe('left'); } } else { // Vertical swipe if (moveY > 0) { performSwipe('down'); } else { performSwipe('up'); } } } else { // Reset card position if swipe wasn't strong enough resetCardPosition(); // We don't trigger click here because the card already has a click event listener } // Hide all hints hideAllHints(); } function updateHints(moveX, moveY) { hideAllHints(); const absX = Math.abs(moveX); const absY = Math.abs(moveY); if (absX > absY) { // Horizontal movement is dominant if (moveX > 0) { rightHint.style.opacity = '1'; } else { leftHint.style.opacity = '1'; } } else { // Vertical movement is dominant if (moveY > 0) { downHint.style.opacity = '1'; } else { upHint.style.opacity = '1'; } } } function hideAllHints() { leftHint.style.opacity = '0'; rightHint.style.opacity = '0'; upHint.style.opacity = '0'; downHint.style.opacity = '0'; } function resetCardPosition() { card.classList.remove('swiping'); card.style.transform = ''; card.style.opacity = '1'; } function performSwipe(direction) { // Add the appropriate swipe class card.classList.add(`swipe-${direction}`); // Start fading out immediately card.style.opacity = '0.8'; // Update the last action text lastActionText.textContent = `Last action: Swiped ${direction}`; // Apply a more dramatic exit animation based on direction if (direction === 'left') { card.style.transform = 'translateX(-350%) rotate(-15deg)'; } else if (direction === 'right') { card.style.transform = 'translateX(350%) rotate(15deg)'; } else if (direction === 'up') { card.style.transform = 'translateY(-350%) rotate(5deg)'; } else if (direction === 'down') { card.style.transform = 'translateY(350%) rotate(-5deg)'; } // Record the selection in the database if we have a current image if (currentImageInfo) { recordSelection(currentImageInfo, direction); } // After animation completes, reset and load a new image setTimeout(() => { card.classList.remove(`swipe-${direction}`); card.classList.remove('swiping'); card.style.transform = ''; // Position the card offscreen to the left (for consistent entry animation) card.style.transform = 'translateX(-100%)'; card.style.opacity = '0'; // Load a new random image from our server loadNewImage(); }, 200); // Reduced from 300ms to 200ms for faster transitions } // Function to record a selection in the database function recordSelection(imageInfo, action) { // Create the data to send const data = { path: imageInfo.path, resolution: imageInfo.resolution, action: action }; // Send the data to the server fetch('/record-selection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => { if (!response.ok) { throw new Error('Failed to record selection'); } return response.json(); }) .then(data => { console.log('Selection recorded:', data); }) .catch(error => { console.error('Error recording selection:', error); }); } // Function to prefetch images and store them in cache function prefetchImages(count = 1) { for (let i = 0; i < count; i++) { // Use a small delay between requests to avoid overwhelming the server setTimeout(() => { console.log(`Prefetching image ${i+1} of ${count}`); fetch('/random-image?t=' + new Date().getTime()) .then(response => { if (!response.ok) throw new Error('Failed to fetch image for prefetching'); return response.json(); }) .then(data => { // Add the fetched image to our cache imageCache.add(data); }) .catch(error => { console.error('Error prefetching image:', error); }); }, i * 200); // Stagger requests by 200ms } } // Function to load a new image from our local server function loadNewImage() { console.log('loadNewImage called'); // Show loading state const img = card.querySelector('img'); img.style.opacity = '0'; // Remove all animation classes to reset card.classList.remove('new-card'); card.classList.remove('new-card-mobile'); // Ensure card is positioned off-screen to the left to start // This guarantees entry from the left regardless of previous swipe direction card.style.transform = 'translateX(-100%)'; card.style.transition = 'none'; // Disable transition when setting initial position // Check if mobile view is active const currentlyMobile = window.matchMedia("(max-width: 768px)").matches; console.log('Current view:', currentlyMobile ? 'mobile' : 'desktop'); // Try to get an image from cache first if (imageCache.images.length > 0) { console.log('Using cached image'); const cachedImage = imageCache.images.shift(); displayImage(cachedImage.data); // Prefetch a new image to replace the one we just used prefetchImages(1); return; } console.log('No cached images available, fetching from server...'); // Fetch a random image from our API with a cache-busting parameter fetch('/random-image?t=' + new Date().getTime()) .then(response => { console.log('Fetch response received:', response.status); if (!response.ok) { throw new Error('Failed to fetch image: ' + response.status); } return response.json(); }) .then(data => { console.log('Image data received:', data); displayImage(data); // After displaying the first image, prefetch more for the cache if (imageCache.images.length < imageCache.maxSize) { prefetchImages(imageCache.maxSize - imageCache.images.length); } }) .catch(error => { console.error('Error fetching random image:', error); // Handle the error (e.g., display an error message) const statusElement = document.querySelector('.status-area p:first-child'); statusElement.textContent = 'Error: Failed to fetch image'; // Try again after a delay setTimeout(() => { console.log('Retrying image load after error...'); loadNewImage(); }, 3000); }); } // Function to display an image (from cache or fresh fetch) function displayImage(data) { // Store current image info currentImageInfo = data; // Extract filename from path const pathParts = data.path.split('/'); const filename = pathParts[pathParts.length - 1]; currentImageInfo.filename = filename; currentImageInfo.creation_date = data.creation_date || 'Unknown'; // Check if mobile view is active const currentlyMobile = window.matchMedia("(max-width: 768px)").matches; // Get the image element const img = card.querySelector('img'); // Set the image source img.src = data.path; // Set up the onload handler img.onload = function() { console.log('Image loaded successfully'); // Force a reflow to ensure animation works void card.offsetWidth; // Re-enable transitions for the animation with faster timing card.style.transition = 'transform 0.25s ease-out, opacity 0.25s ease-out'; // Make the image visible img.style.opacity = '1'; // Different animation approach for mobile vs desktop if (currentlyMobile) { // Simple fade-in for mobile - more reliable card.style.transform = 'translateX(0)'; card.classList.add('new-card-mobile'); } else { // Slide-in animation for desktop card.classList.add('new-card'); } }; // Add error handling for image loading img.onerror = function() { console.error('Failed to load image:', data.path); img.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%3EError Loading Image%3C%2Ftext%3E%3C%2Fsvg%3E'; img.style.opacity = '1'; // Try loading a new image after a delay setTimeout(loadNewImage, 2000); }; // Update status with resolution info const statusElement = document.querySelector('.status-area p:first-child'); statusElement.textContent = `Current resolution: ${data.resolution}`; } // Function to handle card click for viewing full-resolution image function handleCardClick(e) { // Only process click if we have image info and we're not in the middle of a swipe if (!currentImageInfo || card.classList.contains('swiping')) return; // Prevent click from propagating (important for touch devices) if (e) e.stopPropagation(); // Set the full-resolution image source fullscreenImage.src = currentImageInfo.path; // Update modal info modalResolution.textContent = `Resolution: ${currentImageInfo.resolution}`; modalFilename.textContent = `Filename: ${currentImageInfo.filename || 'Unknown'}`; modalCreationDate.textContent = `Creation Date: ${currentImageInfo.creation_date || 'Unknown'}`; // Display the modal modal.style.display = 'block'; } // Load initial image loadNewImage(); });