// Player status bar functionality - Updated to fix logMessage errors // Helper function to normalize file paths for cross-platform compatibility function normalizeFilePath(filePath) { if (!filePath) return ''; // First normalize path separators let normalized = filePath.replace(/\\/g, '/'); // Remove only the patterns directory prefix, not patterns within the path if (normalized.startsWith('./patterns/')) { normalized = normalized.substring(11); } else if (normalized.startsWith('patterns/')) { normalized = normalized.substring(9); } return normalized; } let ws = null; let reconnectAttempts = 0; const maxReconnectAttempts = 5; const reconnectDelay = 3000; // 3 seconds let isEditingSpeed = false; // Track if user is editing speed let playerPreviewData = null; // Store the current pattern's preview data for modal let playerPreviewCtx = null; // Store the canvas context for modal preview let playerAnimationId = null; // Store animation frame ID for modal let lastProgress = 0; // Last known progress from backend let targetProgress = 0; // Target progress to animate towards let animationStartTime = 0; // Start time of current animation let animationDuration = 1000; // Duration of interpolation in ms let smoothAnimationStartTime = 0; // Start time for smooth coordinate animation let smoothAnimationActive = false; // Whether smooth animation is running let modalAnimationId = null; // Store animation frame ID for modal let modalLastProgress = 0; // Last known progress for modal let modalTargetProgress = 0; // Target progress for modal let modalAnimationStartTime = 0; // Start time for modal animation let userDismissedModal = false; // Track if user has manually dismissed the modal // Function to set modal visibility function setModalVisibility(show, userAction = false) { const modal = document.getElementById('playerPreviewModal'); if (!modal) return; if (show) { modal.classList.remove('hidden'); } else { modal.classList.add('hidden'); } if (userAction) { userDismissedModal = !show; } } let currentPreviewFile = null; // Track the current file for preview data function connectWebSocket() { if (ws) { ws.close(); } ws = new WebSocket(`ws://${window.location.host}/ws/status`); ws.onopen = function() { console.log("WebSocket connection established"); reconnectAttempts = 0; }; ws.onclose = function() { console.log("WebSocket connection closed"); if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; setTimeout(connectWebSocket, reconnectDelay); } }; ws.onerror = function(error) { console.error("WebSocket error:", error); }; ws.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === 'status_update') { // Update modal status with the full data syncModalControls(data.data); // Update speed input field on table control page if it exists if (data.data && data.data.speed) { const currentSpeedDisplay = document.getElementById('currentSpeedDisplay'); if (currentSpeedDisplay) { currentSpeedDisplay.textContent = `${data.data.speed} mm/s`; } } // Update connection status dot using 'connection_status' or fallback to 'connected' if (data.data.hasOwnProperty('connection_status')) { updateConnectionStatus(data.data.connection_status); } // Check if current file has changed and reload preview data if needed if (data.data.current_file) { const newFile = normalizeFilePath(data.data.current_file); if (newFile !== currentPreviewFile) { currentPreviewFile = newFile; loadPlayerPreviewData(data.data.current_file); } } else { currentPreviewFile = null; playerPreviewData = null; } // Update progress for modal animation with smooth interpolation if (playerPreviewData && data.data.progress && data.data.progress.percentage !== null) { const newProgress = data.data.progress.percentage / 100; targetProgress = newProgress; // Update modal if open with smooth animation const modal = document.getElementById('playerPreviewModal'); if (modal && !modal.classList.contains('hidden')) { updateModalPreviewSmooth(newProgress); } } // Show modal if pattern is playing and modal is hidden, but only if user hasn't dismissed it if (data.data.current_file && data.data.is_running && !userDismissedModal) { setModalVisibility(true, false); } // Reset userDismissedModal flag if no pattern is playing if (!data.data.current_file || !data.data.is_running) { userDismissedModal = false; } } } catch (error) { console.error("Error processing WebSocket message:", error); } }; } function updateConnectionStatus(isConnected) { const statusDot = document.getElementById("connectionStatusDot"); if (statusDot) { // Update dot color statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${ isConnected ? "bg-green-500" : "bg-red-500" }`; } } // Setup player preview with expand button function setupPlayerPreview() { const previewContainer = document.getElementById('player-pattern-preview'); if (!previewContainer) return; // Get current background image URL const currentBgImage = previewContainer.style.backgroundImage; // Clear container previewContainer.innerHTML = ''; previewContainer.style.backgroundImage = ''; // Create preview image container const imageContainer = document.createElement('div'); imageContainer.className = 'relative aspect-square rounded-full overflow-hidden w-full h-full'; // Create image element const img = document.createElement('img'); img.className = 'w-full h-full object-cover'; // img.alt = 'Pattern Preview'; // Extract URL from background-image CSS img.src = currentBgImage.replace(/^url\(['"](.+)['"]\)$/, '$1'); // Add expand button overlay const expandOverlay = document.createElement('div'); expandOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-200 cursor-pointer z-20 bg-black bg-opacity-20 hover:bg-opacity-30'; expandOverlay.innerHTML = '
fullscreen
'; // Add click handler for expand button expandOverlay.addEventListener('click', (e) => { e.stopPropagation(); openPlayerPreviewModal(); }); // Add image and overlay to image container imageContainer.appendChild(img); imageContainer.appendChild(expandOverlay); // Add image container to preview container previewContainer.appendChild(imageContainer); } // Open player preview modal async function openPlayerPreviewModal() { try { const modal = document.getElementById('playerPreviewModal'); const title = document.getElementById('playerPreviewTitle'); const canvas = document.getElementById('playerPreviewCanvas'); const ctx = canvas.getContext('2d'); const toggleBtn = document.getElementById('toggle-preview-modal-btn'); // Set static title title.textContent = 'Live Pattern Preview'; // Show modal and update toggle button modal.classList.remove('hidden'); // Setup canvas setupPlayerPreviewCanvas(ctx); // Draw initial state drawPlayerPreview(ctx, targetProgress); } catch (error) { console.error(`Error opening player preview modal: ${error.message}`); showStatusMessage('Failed to load pattern for animation', 'error'); } } // Setup player preview canvas for modal function setupPlayerPreviewCanvas(ctx) { const canvas = ctx.canvas; const container = canvas.parentElement; // This is the div with max-w and max-h constraints const modal = document.getElementById('playerPreviewModal'); if (!container || !modal) return; // Calculate available viewport space directly const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Calculate maximum canvas size based on viewport and fixed estimates // Modal uses max-w-5xl (1024px) but we want to be responsive to actual viewport const modalMaxWidth = Math.min(1024, viewportWidth * 0.9); // Account for modal padding const modalMaxHeight = viewportHeight * 0.95; // max-h-[95vh] // Reserve space for modal header (~80px) and controls (~200px) and padding const reservedSpace = 320; // Header + controls + padding const availableModalHeight = modalMaxHeight - reservedSpace; // Calculate canvas constraints (stay within original 800px max, but be responsive) const maxCanvasSize = Math.min(800, modalMaxWidth - 64, availableModalHeight); // 64px for canvas area padding // Ensure minimum size const finalSize = Math.max(200, maxCanvasSize); // Update container to exact size (override CSS constraints) container.style.width = `${finalSize}px`; container.style.height = `${finalSize}px`; container.style.maxWidth = `${finalSize}px`; container.style.maxHeight = `${finalSize}px`; container.style.minWidth = `${finalSize}px`; container.style.minHeight = `${finalSize}px`; // Set the internal canvas size for high-DPI rendering const pixelRatio = (window.devicePixelRatio || 1) * 2; canvas.width = finalSize * pixelRatio; canvas.height = finalSize * pixelRatio; // Set the display size (canvas fills its container) canvas.style.width = '100%'; canvas.style.height = '100%'; console.log('Canvas resized:', { viewport: `${viewportWidth}x${viewportHeight}`, modalMaxWidth, availableModalHeight, finalSize: finalSize }); } // Get interpolated coordinate at specific progress function getInterpolatedCoordinate(progress) { if (!playerPreviewData || playerPreviewData.length === 0) return null; const totalPoints = playerPreviewData.length; const exactIndex = progress * (totalPoints - 1); const index = Math.floor(exactIndex); const fraction = exactIndex - index; // Ensure we don't go out of bounds if (index >= totalPoints - 1) { return playerPreviewData[totalPoints - 1]; } if (index < 0) { return playerPreviewData[0]; } // Get the two coordinates to interpolate between const [theta1, rho1] = playerPreviewData[index]; const [theta2, rho2] = playerPreviewData[index + 1]; // Interpolate theta (handle angle wrapping) let deltaTheta = theta2 - theta1; if (deltaTheta > Math.PI) deltaTheta -= 2 * Math.PI; if (deltaTheta < -Math.PI) deltaTheta += 2 * Math.PI; const interpolatedTheta = theta1 + deltaTheta * fraction; const interpolatedRho = rho1 + (rho2 - rho1) * fraction; return [interpolatedTheta, interpolatedRho]; } // Draw player preview for modal function drawPlayerPreview(ctx, progress) { if (!ctx || !playerPreviewData || playerPreviewData.length === 0) return; const canvas = ctx.canvas; const pixelRatio = (window.devicePixelRatio || 1) * 2; const containerSize = canvas.width / pixelRatio; const center = containerSize / 2; const scale = (containerSize / 2) - 30; ctx.save(); // Clear canvas for fresh drawing ctx.clearRect(0, 0, canvas.width, canvas.height); // Create circular clipping path ctx.beginPath(); ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2); ctx.clip(); // Setup coordinate system for drawing ctx.scale(pixelRatio, pixelRatio); // Calculate how many points to draw const totalPoints = playerPreviewData.length; const pointsToDraw = Math.floor(totalPoints * progress); if (pointsToDraw < 2) { ctx.restore(); return; } // Draw the pattern ctx.beginPath(); ctx.strokeStyle = '#808080'; ctx.lineWidth = 1; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; // Enable high quality rendering ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; // Draw pattern lines up to the last complete segment for (let i = 0; i < pointsToDraw - 1; i++) { const [theta1, rho1] = playerPreviewData[i]; const [theta2, rho2] = playerPreviewData[i + 1]; const x1 = center + rho1 * scale * Math.cos(theta1); const y1 = center + rho1 * scale * Math.sin(theta1); const x2 = center + rho2 * scale * Math.cos(theta2); const y2 = center + rho2 * scale * Math.sin(theta2); if (i === 0) { ctx.moveTo(x1, y1); } ctx.lineTo(x2, y2); } // Draw the final partial segment to the interpolated position if (pointsToDraw > 0) { const interpolatedCoord = getInterpolatedCoordinate(progress); if (interpolatedCoord && pointsToDraw > 1) { // Get the last complete coordinate const [lastTheta, lastRho] = playerPreviewData[pointsToDraw - 1]; const lastX = center + lastRho * scale * Math.cos(lastTheta); const lastY = center + lastRho * scale * Math.sin(lastTheta); // Draw line to interpolated position const [interpTheta, interpRho] = interpolatedCoord; const interpX = center + interpRho * scale * Math.cos(interpTheta); const interpY = center + interpRho * scale * Math.sin(interpTheta); ctx.lineTo(interpX, interpY); } } ctx.stroke(); // Draw current position dot with interpolated position if (pointsToDraw > 0) { const interpolatedCoord = getInterpolatedCoordinate(progress); if (interpolatedCoord) { const [theta, rho] = interpolatedCoord; const x = center + rho * scale * Math.cos(theta); const y = center + rho * scale * Math.sin(theta); // Draw white border ctx.beginPath(); ctx.fillStyle = '#ffffff'; ctx.arc(x, y, 7.5, 0, Math.PI * 2); ctx.fill(); // Draw red dot ctx.beginPath(); ctx.fillStyle = '#ff0000'; ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); } // Load pattern coordinates for player preview async function loadPlayerPreviewData(pattern) { try { const response = await fetch('/get_theta_rho_coordinates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_name: pattern }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } playerPreviewData = data.coordinates; // Store the filename for comparison playerPreviewData.fileName = normalizeFilePath(pattern); } catch (error) { console.error(`Error loading player preview data: ${error.message}`); playerPreviewData = null; } } // Ultra-smooth animation function for modal function animateModalPreview() { const modal = document.getElementById('playerPreviewModal'); if (!modal || modal.classList.contains('hidden')) return; const canvas = document.getElementById('playerPreviewCanvas'); const ctx = canvas.getContext('2d'); if (!ctx || !playerPreviewData) return; const currentTime = Date.now(); const elapsed = currentTime - modalAnimationStartTime; const totalDuration = animationDuration; // Calculate smooth progress with easing const rawProgress = Math.min(elapsed / totalDuration, 1); const easeProgress = rawProgress < 0.5 ? 2 * rawProgress * rawProgress : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2; // Interpolate between last and target progress const currentProgress = modalLastProgress + (modalTargetProgress - modalLastProgress) * easeProgress; // Draw the pattern up to current progress drawPlayerPreview(ctx, currentProgress); // Continue animation if not at target if (rawProgress < 1) { modalAnimationId = requestAnimationFrame(animateModalPreview); } } // Update modal preview with smooth animation function updateModalPreviewSmooth(newProgress) { if (newProgress === modalTargetProgress) return; // No change needed // Stop any existing animation if (modalAnimationId) { cancelAnimationFrame(modalAnimationId); } // Update animation parameters modalLastProgress = modalTargetProgress; modalTargetProgress = newProgress; modalAnimationStartTime = Date.now(); // Start smooth animation animateModalPreview(); } // Setup player preview modal events function setupPlayerPreviewModalEvents() { const modal = document.getElementById('playerPreviewModal'); const closeBtn = document.getElementById('closePlayerPreview'); const toggleBtn = document.getElementById('toggle-preview-modal-btn'); if (!modal || !closeBtn || !toggleBtn) return; // Remove any existing event listeners to prevent conflicts const newToggleBtn = toggleBtn.cloneNode(true); toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn); // Toggle button click handler newToggleBtn.addEventListener('click', () => { const isHidden = modal.classList.contains('hidden'); if (isHidden) { openPlayerPreviewModal(); } else { modal.classList.add('hidden'); } }); // Close modal when clicking close button closeBtn.addEventListener('click', () => { setModalVisibility(false, true); }); // Close modal when clicking outside modal.addEventListener('click', (e) => { if (e.target === modal) { setModalVisibility(false, true); } }); // Close modal with Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !modal.classList.contains('hidden')) { setModalVisibility(false, true); } }); // Setup modal control buttons setupModalControls(); } // Handle pause/resume toggle async function togglePauseResume() { const pauseButton = document.getElementById('modal-pause-button'); if (!pauseButton) return; try { const pauseIcon = pauseButton.querySelector('.material-icons'); const isCurrentlyPaused = pauseIcon.textContent === 'play_arrow'; const endpoint = isCurrentlyPaused ? '/resume_execution' : '/pause_execution'; const response = await fetch(endpoint, { method: 'POST' }); if (!response.ok) throw new Error(`Failed to ${isCurrentlyPaused ? 'resume' : 'pause'}`); } catch (error) { console.error('Error toggling pause:', error); showStatusMessage('Failed to pause/resume pattern', 'error'); } } // Setup modal controls function setupModalControls() { const pauseButton = document.getElementById('modal-pause-button'); const skipButton = document.getElementById('modal-skip-button'); const stopButton = document.getElementById('modal-stop-button'); const speedDisplay = document.getElementById('modal-speed-display'); const speedInput = document.getElementById('modal-speed-input'); if (!pauseButton || !skipButton || !stopButton || !speedDisplay || !speedInput) return; // Pause button click handler pauseButton.addEventListener('click', togglePauseResume); // Skip button click handler skipButton.addEventListener('click', async () => { try { const response = await fetch('/skip_pattern', { method: 'POST' }); if (!response.ok) throw new Error('Failed to skip pattern'); } catch (error) { console.error('Error skipping pattern:', error); showStatusMessage('Failed to skip pattern', 'error'); } }); // Stop button click handler stopButton.addEventListener('click', async () => { try { const response = await fetch('/stop_execution', { method: 'POST' }); if (!response.ok) throw new Error('Failed to stop pattern'); else { // Hide modal when stopping const modal = document.getElementById('playerPreviewModal'); if (modal) modal.classList.add('hidden'); } } catch (error) { console.error('Error stopping pattern:', error); showStatusMessage('Failed to stop pattern', 'error'); } }); // Speed display click to edit speedDisplay.addEventListener('click', () => { isEditingSpeed = true; speedDisplay.classList.add('hidden'); speedInput.value = speedDisplay.textContent; speedInput.classList.remove('hidden'); speedInput.focus(); speedInput.select(); }); // Speed input handlers speedInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); exitModalSpeedEditMode(true); } else if (e.key === 'Escape') { e.preventDefault(); exitModalSpeedEditMode(false); } }); speedInput.addEventListener('blur', () => { exitModalSpeedEditMode(true); }); } // Exit modal speed edit mode async function exitModalSpeedEditMode(save = false) { const speedDisplay = document.getElementById('modal-speed-display'); const speedInput = document.getElementById('modal-speed-input'); if (!speedDisplay || !speedInput) return; isEditingSpeed = false; speedInput.classList.add('hidden'); speedDisplay.classList.remove('hidden'); if (save) { const speed = parseInt(speedInput.value); if (!isNaN(speed) && speed >= 1 && speed <= 5000) { try { const response = await fetch('/set_speed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ speed: speed }) }); const data = await response.json(); if (data.success) { speedDisplay.textContent = speed; showStatusMessage(`Speed set to ${speed} mm/s`, 'success'); } else { throw new Error(data.detail || 'Failed to set speed'); } } catch (error) { console.error('Error setting speed:', error); showStatusMessage('Failed to set speed', 'error'); } } else { showStatusMessage('Please enter a valid speed (1-5000)', 'error'); } } } // Helper function to clean up pattern names function getCleanPatternName(filePath) { if (!filePath) return ''; const fileName = normalizeFilePath(filePath); return fileName.split('/').pop().replace('.thr', ''); } // Sync modal controls with player status function syncModalControls(status) { // Pattern name - clean up to show only filename const modalPatternName = document.getElementById('modal-pattern-name'); if (modalPatternName && status.current_file) { modalPatternName.textContent = getCleanPatternName(status.current_file); } // Pattern preview image const modalPatternPreviewImg = document.getElementById('modal-pattern-preview-img'); if (modalPatternPreviewImg && status.current_file) { const encodedFilename = normalizeFilePath(status.current_file).replace(/[\\/]/g, '--'); const previewUrl = `/preview/${encodedFilename}`; modalPatternPreviewImg.src = previewUrl; } // ETA const modalEta = document.getElementById('modal-eta'); if (modalEta) { if (status.progress && status.progress.remaining_time !== null) { const minutes = Math.floor(status.progress.remaining_time / 60); const seconds = Math.floor(status.progress.remaining_time % 60); modalEta.textContent = `ETA: ${minutes}:${seconds.toString().padStart(2, '0')}`; } else { modalEta.textContent = 'ETA: calculating...'; } } // Progress bar const modalProgressBar = document.getElementById('modal-progress-bar'); if (modalProgressBar) { if (status.progress && status.progress.percentage !== null) { modalProgressBar.style.width = `${status.progress.percentage}%`; } else { modalProgressBar.style.width = '0%'; } } // Next pattern - clean up to show only filename const modalNextPattern = document.getElementById('modal-next-pattern'); if (modalNextPattern) { if (status.playlist && status.playlist.next_file) { modalNextPattern.textContent = getCleanPatternName(status.playlist.next_file); } else { modalNextPattern.textContent = 'None'; } } // Pause button const modalPauseBtn = document.getElementById('modal-pause-button'); if (modalPauseBtn) { const pauseIcon = modalPauseBtn.querySelector('.material-icons'); if (status.is_paused) { pauseIcon.textContent = 'play_arrow'; } else { pauseIcon.textContent = 'pause'; } } // Skip button visibility const modalSkipBtn = document.getElementById('modal-skip-button'); if (modalSkipBtn) { if (status.playlist && status.playlist.next_file) { modalSkipBtn.classList.remove('invisible'); } else { modalSkipBtn.classList.add('invisible'); } } // Speed display const modalSpeedDisplay = document.getElementById('modal-speed-display'); if (modalSpeedDisplay && status.speed && !isEditingSpeed) { modalSpeedDisplay.textContent = status.speed; } } // Toggle modal visibility function togglePreviewModal() { const modal = document.getElementById('playerPreviewModal'); const toggleBtn = document.getElementById('toggle-preview-modal-btn'); if (!modal || !toggleBtn) return; const isHidden = modal.classList.contains('hidden'); if (isHidden) { openPlayerPreviewModal(); } else { setModalVisibility(false, true); toggleBtn.classList.remove('active-tab'); toggleBtn.classList.add('inactive-tab'); } } // Button event handlers document.addEventListener('DOMContentLoaded', async () => { try { // Initialize WebSocket connection initializeWebSocket(); // Setup player preview modal events setupPlayerPreviewModalEvents(); console.log('Player initialized successfully'); } catch (error) { console.error(`Error during initialization: ${error.message}`); } }); // Initialize WebSocket connection function initializeWebSocket() { connectWebSocket(); } // Add resize handler for responsive canvas with debouncing let resizeTimeout; window.addEventListener('resize', () => { const canvas = document.getElementById('playerPreviewCanvas'); const modal = document.getElementById('playerPreviewModal'); if (canvas && modal && !modal.classList.contains('hidden')) { // Clear previous timeout clearTimeout(resizeTimeout); // Debounce resize calls to avoid excessive updates resizeTimeout = setTimeout(() => { const ctx = canvas.getContext('2d'); setupPlayerPreviewCanvas(ctx); drawPlayerPreview(ctx, targetProgress); }, 16); // ~60fps update rate } }); // Handle file changes and reload preview data function handleFileChange(newFile) { if (newFile !== currentPreviewFile) { currentPreviewFile = newFile; if (newFile) { loadPlayerPreviewData(`./patterns/${newFile}`); } else { playerPreviewData = null; } } } // Cache All Previews Prompt functionality let cacheAllInProgress = false; function shouldShowCacheAllPrompt() { // Check if we've already shown the prompt const promptShown = localStorage.getItem('cacheAllPromptShown'); console.log('shouldShowCacheAllPrompt - promptShown:', promptShown); return !promptShown; } function showCacheAllPrompt(forceShow = false) { console.log('showCacheAllPrompt called, forceShow:', forceShow); if (!forceShow && !shouldShowCacheAllPrompt()) { console.log('Cache all prompt already shown, skipping'); return; } const modal = document.getElementById('cacheAllPromptModal'); if (modal) { console.log('Showing cache all prompt modal'); modal.classList.remove('hidden'); // Store whether this was forced (manually triggered) modal.dataset.manuallyTriggered = forceShow.toString(); } else { console.log('Cache all prompt modal not found'); } } function hideCacheAllPrompt() { const modal = document.getElementById('cacheAllPromptModal'); if (modal) { modal.classList.add('hidden'); } } function markCacheAllPromptAsShown() { localStorage.setItem('cacheAllPromptShown', 'true'); } function initializeCacheAllPrompt() { const modal = document.getElementById('cacheAllPromptModal'); const skipBtn = document.getElementById('skipCacheAllBtn'); const startBtn = document.getElementById('startCacheAllBtn'); const closeBtn = document.getElementById('closeCacheAllBtn'); if (!modal || !skipBtn || !startBtn || !closeBtn) { return; } // Skip button handler skipBtn.addEventListener('click', () => { const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true'; hideCacheAllPrompt(); // Only mark as shown if it was automatically shown (not manually triggered) if (!wasManuallyTriggered) { markCacheAllPromptAsShown(); } }); // Close button handler (after completion) closeBtn.addEventListener('click', () => { const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true'; hideCacheAllPrompt(); // Always mark as shown after successful completion if (!wasManuallyTriggered) { markCacheAllPromptAsShown(); } }); // Start caching button handler startBtn.addEventListener('click', async () => { if (cacheAllInProgress) { return; } cacheAllInProgress = true; // Hide buttons and show progress document.getElementById('cacheAllButtons').classList.add('hidden'); document.getElementById('cacheAllProgress').classList.remove('hidden'); try { await startCacheAllProcess(); // Show completion message document.getElementById('cacheAllProgress').classList.add('hidden'); document.getElementById('cacheAllComplete').classList.remove('hidden'); } catch (error) { console.error('Error caching all previews:', error); // Show error and reset document.getElementById('cacheAllProgressText').textContent = 'Error occurred during caching'; setTimeout(() => { hideCacheAllPrompt(); markCacheAllPromptAsShown(); }, 3000); } finally { cacheAllInProgress = false; } }); } async function startCacheAllProcess() { try { // Get list of patterns const response = await fetch('/list_theta_rho_files'); const patterns = await response.json(); if (!patterns || patterns.length === 0) { throw new Error('No patterns found'); } const progressBar = document.getElementById('cacheAllProgressBar'); const progressText = document.getElementById('cacheAllProgressText'); const progressPercentage = document.getElementById('cacheAllProgressPercentage'); let completed = 0; const batchSize = 5; // Process in small batches to avoid overwhelming the server for (let i = 0; i < patterns.length; i += batchSize) { const batch = patterns.slice(i, i + batchSize); // Update progress text progressText.textContent = `Caching previews... (${Math.min(i + batchSize, patterns.length)}/${patterns.length})`; // Process batch const batchPromises = batch.map(async (pattern) => { try { const previewResponse = await fetch('/preview_thr', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ file_name: pattern }) }); if (previewResponse.ok) { const data = await previewResponse.json(); if (data.preview_url) { // Pre-load the image to cache it return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(); img.onerror = () => resolve(); // Continue even if image fails img.src = data.preview_url; }); } } return Promise.resolve(); } catch (error) { console.warn(`Failed to cache preview for ${pattern}:`, error); return Promise.resolve(); // Continue with other patterns } }); await Promise.all(batchPromises); completed += batch.length; // Update progress bar const progress = Math.round((completed / patterns.length) * 100); progressBar.style.width = `${progress}%`; progressPercentage.textContent = `${progress}%`; // Small delay between batches to prevent overwhelming the server if (i + batchSize < patterns.length) { await new Promise(resolve => setTimeout(resolve, 100)); } } progressText.textContent = `Completed! Cached ${patterns.length} previews.`; } catch (error) { throw error; } } // Function to be called after initial cache generation completes function onInitialCacheComplete() { console.log('onInitialCacheComplete called'); // Show the cache all prompt after a short delay setTimeout(() => { console.log('Triggering cache all prompt after delay'); showCacheAllPrompt(); }, 1000); } // Initialize on DOM load document.addEventListener('DOMContentLoaded', () => { initializeCacheAllPrompt(); }); // Make functions available globally for debugging window.onInitialCacheComplete = onInitialCacheComplete; window.showCacheAllPrompt = showCacheAllPrompt; window.testCacheAllPrompt = function() { console.log('Manual test trigger'); // Clear localStorage for testing localStorage.removeItem('cacheAllPromptShown'); showCacheAllPrompt(); };