1
0

base.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213
  1. // Player status bar functionality - Updated to fix logMessage errors
  2. // Update LED nav label based on provider
  3. async function updateLedNavLabel() {
  4. try {
  5. const response = await fetch('/get_led_config');
  6. if (response.ok) {
  7. const data = await response.json();
  8. const navLabel = document.getElementById('led-nav-label');
  9. if (navLabel) {
  10. if (data.provider === 'wled') {
  11. navLabel.textContent = 'WLED';
  12. } else if (data.provider === 'dw_leds') {
  13. navLabel.textContent = 'DW LEDs';
  14. } else {
  15. navLabel.textContent = 'LED';
  16. }
  17. }
  18. }
  19. } catch (error) {
  20. console.error('Error updating LED nav label:', error);
  21. }
  22. }
  23. // Call on page load
  24. document.addEventListener('DOMContentLoaded', updateLedNavLabel);
  25. // Pattern files cache for improved performance with localStorage persistence
  26. const PATTERN_CACHE_KEY = 'dune_weaver_pattern_files_cache';
  27. const PATTERN_CACHE_EXPIRY = 30 * 60 * 1000; // 30 minutes cache (longer since it persists)
  28. // Function to get cached pattern files or fetch fresh data
  29. async function getCachedPatternFiles(forceRefresh = false) {
  30. const now = Date.now();
  31. // Try to load from localStorage first
  32. if (!forceRefresh) {
  33. try {
  34. const cachedData = localStorage.getItem(PATTERN_CACHE_KEY);
  35. if (cachedData) {
  36. const { files, timestamp } = JSON.parse(cachedData);
  37. if (files && timestamp && (now - timestamp) < PATTERN_CACHE_EXPIRY) {
  38. console.log('Using cached pattern files from localStorage');
  39. return files;
  40. }
  41. }
  42. } catch (error) {
  43. console.warn('Error reading pattern files cache from localStorage:', error);
  44. }
  45. }
  46. try {
  47. console.log('Fetching fresh pattern files from server');
  48. const response = await fetch('/list_theta_rho_files');
  49. if (!response.ok) {
  50. throw new Error(`Failed to fetch pattern files: ${response.status}`);
  51. }
  52. const files = await response.json();
  53. // Store in localStorage
  54. try {
  55. const cacheData = { files, timestamp: now };
  56. localStorage.setItem(PATTERN_CACHE_KEY, JSON.stringify(cacheData));
  57. } catch (error) {
  58. console.warn('Error storing pattern files cache in localStorage:', error);
  59. }
  60. return files;
  61. } catch (error) {
  62. console.error('Error fetching pattern files:', error);
  63. // Try to return any cached data as fallback, even if expired
  64. try {
  65. const cachedData = localStorage.getItem(PATTERN_CACHE_KEY);
  66. if (cachedData) {
  67. const { files } = JSON.parse(cachedData);
  68. if (files) {
  69. console.log('Using expired cached pattern files as fallback');
  70. return files;
  71. }
  72. }
  73. } catch (fallbackError) {
  74. console.warn('Error reading fallback cache:', fallbackError);
  75. }
  76. return [];
  77. }
  78. }
  79. // Function to invalidate pattern files cache
  80. function invalidatePatternFilesCache() {
  81. try {
  82. localStorage.removeItem(PATTERN_CACHE_KEY);
  83. console.log('Pattern files cache invalidated');
  84. } catch (error) {
  85. console.warn('Error invalidating pattern files cache:', error);
  86. }
  87. }
  88. // Helper function to normalize file paths for cross-platform compatibility
  89. function normalizeFilePath(filePath) {
  90. if (!filePath) return '';
  91. // First normalize path separators
  92. let normalized = filePath.replace(/\\/g, '/');
  93. // Remove only the patterns directory prefix, not patterns within the path
  94. if (normalized.startsWith('./patterns/')) {
  95. normalized = normalized.substring(11);
  96. } else if (normalized.startsWith('patterns/')) {
  97. normalized = normalized.substring(9);
  98. }
  99. return normalized;
  100. }
  101. let ws = null;
  102. let reconnectAttempts = 0;
  103. const maxReconnectAttempts = 5;
  104. const reconnectDelay = 3000; // 3 seconds
  105. let isEditingSpeed = false; // Track if user is editing speed
  106. // WebSocket UI update throttling for Pi performance
  107. let lastUIUpdate = 0;
  108. const UI_UPDATE_INTERVAL = 100; // Minimum ms between UI updates (10 updates/sec max)
  109. let playerPreviewData = null; // Store the current pattern's preview data for modal
  110. let playerPreviewCtx = null; // Store the canvas context for modal preview
  111. let playerAnimationId = null; // Store animation frame ID for modal
  112. let lastProgress = 0; // Last known progress from backend
  113. let targetProgress = 0; // Target progress to animate towards
  114. let animationStartTime = 0; // Start time of current animation
  115. let animationDuration = 1000; // Duration of interpolation in ms
  116. let smoothAnimationStartTime = 0; // Start time for smooth coordinate animation
  117. let smoothAnimationActive = false; // Whether smooth animation is running
  118. let modalAnimationId = null; // Store animation frame ID for modal
  119. let modalLastProgress = 0; // Last known progress for modal
  120. let modalTargetProgress = 0; // Target progress for modal
  121. let modalAnimationStartTime = 0; // Start time for modal animation
  122. let userDismissedModal = false; // Track if user has manually dismissed the modal
  123. // Function to set modal visibility
  124. function setModalVisibility(show, userAction = false) {
  125. const modal = document.getElementById('playerPreviewModal');
  126. if (!modal) return;
  127. if (show) {
  128. modal.classList.remove('hidden');
  129. } else {
  130. modal.classList.add('hidden');
  131. }
  132. if (userAction) {
  133. userDismissedModal = !show;
  134. }
  135. }
  136. let currentPreviewFile = null; // Track the current file for preview data
  137. // Global playback status for cross-file access
  138. window.currentPlaybackStatus = {
  139. is_running: false,
  140. current_file: null
  141. };
  142. function connectWebSocket() {
  143. if (ws) {
  144. ws.close();
  145. }
  146. ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws/status`);
  147. ws.onopen = function() {
  148. console.log("WebSocket connection established");
  149. reconnectAttempts = 0;
  150. };
  151. ws.onclose = function() {
  152. console.log("WebSocket connection closed");
  153. if (reconnectAttempts < maxReconnectAttempts) {
  154. reconnectAttempts++;
  155. setTimeout(connectWebSocket, reconnectDelay);
  156. }
  157. };
  158. ws.onerror = function(error) {
  159. console.error("WebSocket error:", error);
  160. };
  161. ws.onmessage = function(event) {
  162. try {
  163. const data = JSON.parse(event.data);
  164. if (data.type === 'status_update') {
  165. // Always update global playback status (not throttled)
  166. // This ensures play button always has current state
  167. window.currentPlaybackStatus = {
  168. is_running: data.data.is_running || false,
  169. current_file: data.data.current_file || null
  170. };
  171. // Throttle UI updates for better Pi performance
  172. const now = Date.now();
  173. if (now - lastUIUpdate < UI_UPDATE_INTERVAL) {
  174. return; // Skip this update, too soon
  175. }
  176. lastUIUpdate = now;
  177. // Update modal status with the full data
  178. syncModalControls(data.data);
  179. // Update speed input field on table control page if it exists
  180. if (data.data && data.data.speed) {
  181. const currentSpeedDisplay = document.getElementById('currentSpeedDisplay');
  182. if (currentSpeedDisplay) {
  183. currentSpeedDisplay.textContent = `${data.data.speed} mm/s`;
  184. }
  185. }
  186. // Update connection status dot using 'connection_status' or fallback to 'connected'
  187. if (data.data.hasOwnProperty('connection_status')) {
  188. updateConnectionStatus(data.data.connection_status);
  189. }
  190. // Check if current file has changed and reload preview data if needed
  191. if (data.data.current_file) {
  192. const newFile = normalizeFilePath(data.data.current_file);
  193. if (newFile !== currentPreviewFile) {
  194. currentPreviewFile = newFile;
  195. // Only preload if we're on the browse page (index.html)
  196. // Other pages (playlists, table_control, LED, settings) will load on-demand
  197. const modal = document.getElementById('playerPreviewModal');
  198. const browsePage = document.getElementById('browseSortFieldSelect');
  199. if (modal && browsePage) {
  200. // We're on the browse page with the modal - preload coordinates
  201. loadPlayerPreviewData(data.data.current_file);
  202. }
  203. }
  204. } else {
  205. currentPreviewFile = null;
  206. playerPreviewData = null;
  207. }
  208. // Update progress for modal animation with smooth interpolation
  209. if (playerPreviewData && data.data.progress && data.data.progress.percentage !== null) {
  210. const newProgress = data.data.progress.percentage / 100;
  211. targetProgress = newProgress;
  212. // Update modal if open with smooth animation
  213. const modal = document.getElementById('playerPreviewModal');
  214. if (modal && !modal.classList.contains('hidden')) {
  215. updateModalPreviewSmooth(newProgress);
  216. }
  217. }
  218. // Reset userDismissedModal flag if no pattern is playing
  219. if (!data.data.current_file || !data.data.is_running) {
  220. userDismissedModal = false;
  221. }
  222. }
  223. } catch (error) {
  224. console.error("Error processing WebSocket message:", error);
  225. }
  226. };
  227. }
  228. function updateConnectionStatus(isConnected) {
  229. const statusDot = document.getElementById("connectionStatusDot");
  230. if (statusDot) {
  231. // Update dot color
  232. statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${
  233. isConnected ? "bg-green-500" : "bg-red-500"
  234. }`;
  235. }
  236. }
  237. // Setup player preview with expand button
  238. function setupPlayerPreview() {
  239. const previewContainer = document.getElementById('player-pattern-preview');
  240. if (!previewContainer) return;
  241. // Get current background image URL
  242. const currentBgImage = previewContainer.style.backgroundImage;
  243. // Clear container
  244. previewContainer.innerHTML = '';
  245. previewContainer.style.backgroundImage = '';
  246. // Create preview image container
  247. const imageContainer = document.createElement('div');
  248. imageContainer.className = 'relative aspect-square rounded-full overflow-hidden w-full h-full';
  249. // Create image element
  250. const img = document.createElement('img');
  251. img.className = 'w-full h-full object-cover';
  252. // img.alt = 'Pattern Preview';
  253. // Extract URL from background-image CSS
  254. img.src = currentBgImage.replace(/^url\(['"](.+)['"]\)$/, '$1');
  255. // Add expand button overlay
  256. const expandOverlay = document.createElement('div');
  257. 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';
  258. expandOverlay.innerHTML = '<div class="bg-white rounded-full p-3 shadow-lg flex items-center justify-center w-12 h-12"><span class="material-icons text-xl text-gray-800">fullscreen</span></div>';
  259. // Add click handler for expand button
  260. expandOverlay.addEventListener('click', (e) => {
  261. e.stopPropagation();
  262. openPlayerPreviewModal();
  263. });
  264. // Add image and overlay to image container
  265. imageContainer.appendChild(img);
  266. imageContainer.appendChild(expandOverlay);
  267. // Add image container to preview container
  268. previewContainer.appendChild(imageContainer);
  269. }
  270. // Open player preview modal
  271. async function openPlayerPreviewModal() {
  272. try {
  273. const modal = document.getElementById('playerPreviewModal');
  274. const title = document.getElementById('playerPreviewTitle');
  275. const canvas = document.getElementById('playerPreviewCanvas');
  276. const ctx = canvas.getContext('2d');
  277. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  278. // Show modal immediately for instant feedback
  279. modal.classList.remove('hidden');
  280. // Setup canvas (so it's ready to display loading state)
  281. setupPlayerPreviewCanvas(ctx);
  282. // Load preview data on-demand if not already loaded
  283. if (!playerPreviewData && currentPreviewFile) {
  284. // Show loading state
  285. title.textContent = 'Loading pattern...';
  286. drawLoadingState(ctx);
  287. // Load data in background
  288. await loadPlayerPreviewData(`./patterns/${currentPreviewFile}`);
  289. // Update title when loaded
  290. title.textContent = 'Live Pattern Preview';
  291. } else {
  292. // Data already loaded
  293. title.textContent = 'Live Pattern Preview';
  294. }
  295. // Draw the pattern (either immediately if cached, or after loading)
  296. drawPlayerPreview(ctx, targetProgress);
  297. } catch (error) {
  298. console.error(`Error opening player preview modal: ${error.message}`);
  299. showStatusMessage('Failed to load pattern for animation', 'error');
  300. }
  301. }
  302. // Setup player preview canvas for modal
  303. function setupPlayerPreviewCanvas(ctx) {
  304. const canvas = ctx.canvas;
  305. const container = canvas.parentElement; // This is the div with max-w and max-h constraints
  306. const modal = document.getElementById('playerPreviewModal');
  307. if (!container || !modal) return;
  308. // Calculate available viewport space directly
  309. const viewportWidth = window.innerWidth;
  310. const viewportHeight = window.innerHeight;
  311. // Calculate maximum canvas size based on viewport and fixed estimates
  312. // Modal uses max-w-5xl (1024px) but we want to be responsive to actual viewport
  313. const modalMaxWidth = Math.min(1024, viewportWidth * 0.9); // Account for modal padding
  314. const modalMaxHeight = viewportHeight * 0.95; // max-h-[95vh]
  315. // Reserve space for modal header (~80px) and controls (~200px) and padding
  316. const reservedSpace = 320; // Header + controls + padding
  317. const availableModalHeight = modalMaxHeight - reservedSpace;
  318. // Calculate canvas constraints (stay within original 800px max, but be responsive)
  319. const maxCanvasSize = Math.min(800, modalMaxWidth - 64, availableModalHeight); // 64px for canvas area padding
  320. // Ensure minimum size
  321. const finalSize = Math.max(200, maxCanvasSize);
  322. // Update container to exact size (override CSS constraints)
  323. container.style.width = `${finalSize}px`;
  324. container.style.height = `${finalSize}px`;
  325. container.style.maxWidth = `${finalSize}px`;
  326. container.style.maxHeight = `${finalSize}px`;
  327. container.style.minWidth = `${finalSize}px`;
  328. container.style.minHeight = `${finalSize}px`;
  329. // Set the internal canvas size for high-DPI rendering
  330. // Cap at 1.5x for better Pi performance (was 2x forced)
  331. const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
  332. canvas.width = finalSize * pixelRatio;
  333. canvas.height = finalSize * pixelRatio;
  334. // Set the display size (canvas fills its container)
  335. canvas.style.width = '100%';
  336. canvas.style.height = '100%';
  337. console.log('Canvas resized:', {
  338. viewport: `${viewportWidth}x${viewportHeight}`,
  339. modalMaxWidth,
  340. availableModalHeight,
  341. finalSize: finalSize
  342. });
  343. }
  344. // Get interpolated coordinate at specific progress
  345. function getInterpolatedCoordinate(progress) {
  346. if (!playerPreviewData || playerPreviewData.length === 0) return null;
  347. const totalPoints = playerPreviewData.length;
  348. const exactIndex = progress * (totalPoints - 1);
  349. const index = Math.floor(exactIndex);
  350. const fraction = exactIndex - index;
  351. // Ensure we don't go out of bounds
  352. if (index >= totalPoints - 1) {
  353. return playerPreviewData[totalPoints - 1];
  354. }
  355. if (index < 0) {
  356. return playerPreviewData[0];
  357. }
  358. // Get the two coordinates to interpolate between
  359. const [theta1, rho1] = playerPreviewData[index];
  360. const [theta2, rho2] = playerPreviewData[index + 1];
  361. // Interpolate theta (handle angle wrapping)
  362. let deltaTheta = theta2 - theta1;
  363. if (deltaTheta > Math.PI) deltaTheta -= 2 * Math.PI;
  364. if (deltaTheta < -Math.PI) deltaTheta += 2 * Math.PI;
  365. const interpolatedTheta = theta1 + deltaTheta * fraction;
  366. const interpolatedRho = rho1 + (rho2 - rho1) * fraction;
  367. return [interpolatedTheta, interpolatedRho];
  368. }
  369. // Draw loading state on canvas
  370. function drawLoadingState(ctx) {
  371. if (!ctx) return;
  372. const canvas = ctx.canvas;
  373. // Must match the pixelRatio used when setting canvas size
  374. const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
  375. const containerSize = canvas.width / pixelRatio;
  376. const center = containerSize / 2;
  377. ctx.save();
  378. // Clear canvas
  379. ctx.clearRect(0, 0, canvas.width, canvas.height);
  380. // Create circular clipping path
  381. ctx.beginPath();
  382. ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
  383. ctx.clip();
  384. // Setup coordinate system
  385. ctx.scale(pixelRatio, pixelRatio);
  386. // Draw loading text only
  387. ctx.fillStyle = '#9ca3af';
  388. ctx.font = '16px sans-serif';
  389. ctx.textAlign = 'center';
  390. ctx.textBaseline = 'middle';
  391. ctx.fillText('Loading pattern...', center, center);
  392. ctx.restore();
  393. }
  394. // Draw player preview for modal
  395. function drawPlayerPreview(ctx, progress) {
  396. if (!ctx || !playerPreviewData || playerPreviewData.length === 0) return;
  397. const canvas = ctx.canvas;
  398. // Must match the pixelRatio used when setting canvas size
  399. const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
  400. const containerSize = canvas.width / pixelRatio;
  401. const center = containerSize / 2;
  402. const scale = (containerSize / 2) - 30;
  403. ctx.save();
  404. // Clear canvas for fresh drawing
  405. ctx.clearRect(0, 0, canvas.width, canvas.height);
  406. // Create circular clipping path
  407. ctx.beginPath();
  408. ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
  409. ctx.clip();
  410. // Setup coordinate system for drawing
  411. ctx.scale(pixelRatio, pixelRatio);
  412. // Calculate how many points to draw
  413. const totalPoints = playerPreviewData.length;
  414. const pointsToDraw = Math.floor(totalPoints * progress);
  415. if (pointsToDraw < 2) {
  416. ctx.restore();
  417. return;
  418. }
  419. // Draw the pattern
  420. ctx.beginPath();
  421. ctx.strokeStyle = '#808080';
  422. ctx.lineWidth = 1;
  423. ctx.lineCap = 'round';
  424. ctx.lineJoin = 'round';
  425. // Enable high quality rendering
  426. ctx.imageSmoothingEnabled = true;
  427. ctx.imageSmoothingQuality = 'high';
  428. // Draw pattern lines up to the last complete segment
  429. for (let i = 0; i < pointsToDraw - 1; i++) {
  430. const [theta1, rho1] = playerPreviewData[i];
  431. const [theta2, rho2] = playerPreviewData[i + 1];
  432. const x1 = center + rho1 * scale * Math.cos(theta1);
  433. const y1 = center + rho1 * scale * Math.sin(theta1);
  434. const x2 = center + rho2 * scale * Math.cos(theta2);
  435. const y2 = center + rho2 * scale * Math.sin(theta2);
  436. if (i === 0) {
  437. ctx.moveTo(x1, y1);
  438. }
  439. ctx.lineTo(x2, y2);
  440. }
  441. // Draw the final partial segment to the interpolated position
  442. if (pointsToDraw > 0) {
  443. const interpolatedCoord = getInterpolatedCoordinate(progress);
  444. if (interpolatedCoord && pointsToDraw > 1) {
  445. // Get the last complete coordinate
  446. const [lastTheta, lastRho] = playerPreviewData[pointsToDraw - 1];
  447. const lastX = center + lastRho * scale * Math.cos(lastTheta);
  448. const lastY = center + lastRho * scale * Math.sin(lastTheta);
  449. // Draw line to interpolated position
  450. const [interpTheta, interpRho] = interpolatedCoord;
  451. const interpX = center + interpRho * scale * Math.cos(interpTheta);
  452. const interpY = center + interpRho * scale * Math.sin(interpTheta);
  453. ctx.lineTo(interpX, interpY);
  454. }
  455. }
  456. ctx.stroke();
  457. // Draw current position dot with interpolated position
  458. if (pointsToDraw > 0) {
  459. const interpolatedCoord = getInterpolatedCoordinate(progress);
  460. if (interpolatedCoord) {
  461. const [theta, rho] = interpolatedCoord;
  462. const x = center + rho * scale * Math.cos(theta);
  463. const y = center + rho * scale * Math.sin(theta);
  464. // Draw white border
  465. ctx.beginPath();
  466. ctx.fillStyle = '#ffffff';
  467. ctx.arc(x, y, 7.5, 0, Math.PI * 2);
  468. ctx.fill();
  469. // Draw red dot
  470. ctx.beginPath();
  471. ctx.fillStyle = '#ff0000';
  472. ctx.arc(x, y, 6, 0, Math.PI * 2);
  473. ctx.fill();
  474. }
  475. }
  476. ctx.restore();
  477. }
  478. // Load pattern coordinates for player preview
  479. async function loadPlayerPreviewData(pattern) {
  480. try {
  481. const response = await fetch('/get_theta_rho_coordinates', {
  482. method: 'POST',
  483. headers: { 'Content-Type': 'application/json' },
  484. body: JSON.stringify({ file_name: pattern })
  485. });
  486. if (!response.ok) {
  487. throw new Error(`HTTP error! status: ${response.status}`);
  488. }
  489. const data = await response.json();
  490. if (data.error) {
  491. throw new Error(data.error);
  492. }
  493. playerPreviewData = data.coordinates;
  494. // Store the filename for comparison
  495. playerPreviewData.fileName = normalizeFilePath(pattern);
  496. } catch (error) {
  497. console.error(`Error loading player preview data: ${error.message}`);
  498. playerPreviewData = null;
  499. }
  500. }
  501. // Ultra-smooth animation function for modal
  502. function animateModalPreview() {
  503. const modal = document.getElementById('playerPreviewModal');
  504. if (!modal || modal.classList.contains('hidden')) return;
  505. const canvas = document.getElementById('playerPreviewCanvas');
  506. const ctx = canvas.getContext('2d');
  507. if (!ctx || !playerPreviewData) return;
  508. const currentTime = Date.now();
  509. const elapsed = currentTime - modalAnimationStartTime;
  510. const totalDuration = animationDuration;
  511. // Calculate smooth progress with easing
  512. const rawProgress = Math.min(elapsed / totalDuration, 1);
  513. const easeProgress = rawProgress < 0.5
  514. ? 2 * rawProgress * rawProgress
  515. : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2;
  516. // Interpolate between last and target progress
  517. const currentProgress = modalLastProgress + (modalTargetProgress - modalLastProgress) * easeProgress;
  518. // Draw the pattern up to current progress
  519. drawPlayerPreview(ctx, currentProgress);
  520. // Continue animation if not at target
  521. if (rawProgress < 1) {
  522. modalAnimationId = requestAnimationFrame(animateModalPreview);
  523. }
  524. }
  525. // Update modal preview with smooth animation
  526. function updateModalPreviewSmooth(newProgress) {
  527. if (newProgress === modalTargetProgress) return; // No change needed
  528. // Stop any existing animation
  529. if (modalAnimationId) {
  530. cancelAnimationFrame(modalAnimationId);
  531. }
  532. // Update animation parameters
  533. modalLastProgress = modalTargetProgress;
  534. modalTargetProgress = newProgress;
  535. modalAnimationStartTime = Date.now();
  536. // Start smooth animation
  537. animateModalPreview();
  538. }
  539. // Setup player preview modal events
  540. function setupPlayerPreviewModalEvents() {
  541. const modal = document.getElementById('playerPreviewModal');
  542. const closeBtn = document.getElementById('closePlayerPreview');
  543. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  544. if (!modal || !closeBtn || !toggleBtn) return;
  545. // Remove any existing event listeners to prevent conflicts
  546. const newToggleBtn = toggleBtn.cloneNode(true);
  547. toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
  548. // Toggle button click handler
  549. newToggleBtn.addEventListener('click', () => {
  550. const isHidden = modal.classList.contains('hidden');
  551. if (isHidden) {
  552. openPlayerPreviewModal();
  553. } else {
  554. modal.classList.add('hidden');
  555. }
  556. });
  557. // Close modal when clicking close button
  558. closeBtn.addEventListener('click', () => {
  559. setModalVisibility(false, true);
  560. });
  561. // Close modal when clicking outside
  562. modal.addEventListener('click', (e) => {
  563. if (e.target === modal) {
  564. setModalVisibility(false, true);
  565. }
  566. });
  567. // Close modal with Escape key
  568. document.addEventListener('keydown', (e) => {
  569. if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
  570. setModalVisibility(false, true);
  571. }
  572. });
  573. // Setup modal control buttons
  574. setupModalControls();
  575. }
  576. // Handle pause/resume toggle
  577. async function togglePauseResume() {
  578. const pauseButton = document.getElementById('modal-pause-button');
  579. if (!pauseButton) return;
  580. try {
  581. const pauseIcon = pauseButton.querySelector('.material-icons');
  582. const isCurrentlyPaused = pauseIcon.textContent === 'play_arrow';
  583. // Show immediate feedback
  584. showStatusMessage(isCurrentlyPaused ? 'Resuming...' : 'Pausing...', 'info');
  585. const endpoint = isCurrentlyPaused ? '/resume_execution' : '/pause_execution';
  586. const response = await fetch(endpoint, { method: 'POST' });
  587. if (!response.ok) throw new Error(`Failed to ${isCurrentlyPaused ? 'resume' : 'pause'}`);
  588. // Show success message
  589. showStatusMessage(isCurrentlyPaused ? 'Pattern resumed' : 'Pattern paused', 'success');
  590. } catch (error) {
  591. console.error('Error toggling pause:', error);
  592. showStatusMessage('Failed to pause/resume pattern', 'error');
  593. }
  594. }
  595. // Setup modal controls
  596. function setupModalControls() {
  597. const pauseButton = document.getElementById('modal-pause-button');
  598. const skipButton = document.getElementById('modal-skip-button');
  599. const stopButton = document.getElementById('modal-stop-button');
  600. const speedDisplay = document.getElementById('modal-speed-display');
  601. const speedInput = document.getElementById('modal-speed-input');
  602. if (!pauseButton || !skipButton || !stopButton || !speedDisplay || !speedInput) return;
  603. // Pause button click handler
  604. pauseButton.addEventListener('click', togglePauseResume);
  605. // Skip button click handler
  606. skipButton.addEventListener('click', async () => {
  607. try {
  608. // Show immediate feedback
  609. showStatusMessage('Skipping to next pattern...', 'info');
  610. const response = await fetch('/skip_pattern', { method: 'POST' });
  611. if (!response.ok) throw new Error('Failed to skip pattern');
  612. // Show success message
  613. showStatusMessage('Skipped to next pattern', 'success');
  614. } catch (error) {
  615. console.error('Error skipping pattern:', error);
  616. showStatusMessage('Failed to skip pattern', 'error');
  617. }
  618. });
  619. // Stop button click handler
  620. stopButton.addEventListener('click', async () => {
  621. try {
  622. // Show immediate feedback
  623. showStatusMessage('Stopping...', 'info');
  624. const response = await fetch('/stop_execution', { method: 'POST' });
  625. if (!response.ok) throw new Error('Failed to stop pattern');
  626. else {
  627. // Show success message
  628. showStatusMessage('Pattern stopped', 'success');
  629. // Hide modal when stopping
  630. const modal = document.getElementById('playerPreviewModal');
  631. if (modal) modal.classList.add('hidden');
  632. }
  633. } catch (error) {
  634. console.error('Error stopping pattern:', error);
  635. showStatusMessage('Failed to stop pattern', 'error');
  636. }
  637. });
  638. // Speed display click to edit
  639. speedDisplay.addEventListener('click', () => {
  640. isEditingSpeed = true;
  641. speedDisplay.classList.add('hidden');
  642. speedInput.value = speedDisplay.textContent;
  643. speedInput.classList.remove('hidden');
  644. speedInput.focus();
  645. speedInput.select();
  646. });
  647. // Speed input handlers
  648. speedInput.addEventListener('keydown', (e) => {
  649. if (e.key === 'Enter') {
  650. e.preventDefault();
  651. exitModalSpeedEditMode(true);
  652. } else if (e.key === 'Escape') {
  653. e.preventDefault();
  654. exitModalSpeedEditMode(false);
  655. }
  656. });
  657. speedInput.addEventListener('blur', () => {
  658. exitModalSpeedEditMode(true);
  659. });
  660. }
  661. // Exit modal speed edit mode
  662. async function exitModalSpeedEditMode(save = false) {
  663. const speedDisplay = document.getElementById('modal-speed-display');
  664. const speedInput = document.getElementById('modal-speed-input');
  665. if (!speedDisplay || !speedInput) return;
  666. isEditingSpeed = false;
  667. speedInput.classList.add('hidden');
  668. speedDisplay.classList.remove('hidden');
  669. if (save) {
  670. const speed = parseInt(speedInput.value);
  671. if (!isNaN(speed) && speed >= 1 && speed <= 5000) {
  672. try {
  673. const response = await fetch('/set_speed', {
  674. method: 'POST',
  675. headers: { 'Content-Type': 'application/json' },
  676. body: JSON.stringify({ speed: speed })
  677. });
  678. const data = await response.json();
  679. if (data.success) {
  680. speedDisplay.textContent = speed;
  681. showStatusMessage(`Speed set to ${speed} mm/s`, 'success');
  682. } else {
  683. throw new Error(data.detail || 'Failed to set speed');
  684. }
  685. } catch (error) {
  686. console.error('Error setting speed:', error);
  687. showStatusMessage('Failed to set speed', 'error');
  688. }
  689. } else {
  690. showStatusMessage('Please enter a valid speed (1-5000)', 'error');
  691. }
  692. }
  693. }
  694. // Helper function to clean up pattern names
  695. function getCleanPatternName(filePath) {
  696. if (!filePath) return '';
  697. const fileName = normalizeFilePath(filePath);
  698. return fileName.split('/').pop().replace('.thr', '');
  699. }
  700. // Sync modal controls with player status
  701. function syncModalControls(status) {
  702. // Pattern name - clean up to show only filename
  703. const modalPatternName = document.getElementById('modal-pattern-name');
  704. if (modalPatternName && status.current_file) {
  705. modalPatternName.textContent = getCleanPatternName(status.current_file);
  706. }
  707. // Pattern preview image
  708. const modalPatternPreviewImg = document.getElementById('modal-pattern-preview-img');
  709. if (modalPatternPreviewImg && status.current_file) {
  710. const encodedFilename = normalizeFilePath(status.current_file).replace(/[\\/]/g, '--');
  711. const previewUrl = `/preview/${encodedFilename}`;
  712. modalPatternPreviewImg.src = previewUrl;
  713. }
  714. // ETA or Pause Countdown
  715. const modalEta = document.getElementById('modal-eta');
  716. if (modalEta) {
  717. // Check if we're in a pause between patterns
  718. if (status.pause_time_remaining && status.pause_time_remaining > 0) {
  719. const minutes = Math.floor(status.pause_time_remaining / 60);
  720. const seconds = Math.floor(status.pause_time_remaining % 60);
  721. modalEta.textContent = `Next in: ${minutes}:${seconds.toString().padStart(2, '0')}`;
  722. } else if (status.progress && status.progress.remaining_time !== null) {
  723. const minutes = Math.floor(status.progress.remaining_time / 60);
  724. const seconds = Math.floor(status.progress.remaining_time % 60);
  725. modalEta.textContent = `ETA: ${minutes}:${seconds.toString().padStart(2, '0')}`;
  726. } else {
  727. modalEta.textContent = 'ETA: calculating...';
  728. }
  729. }
  730. // Progress bar
  731. const modalProgressBar = document.getElementById('modal-progress-bar');
  732. if (modalProgressBar) {
  733. if (status.progress && status.progress.percentage !== null) {
  734. modalProgressBar.style.width = `${status.progress.percentage}%`;
  735. } else {
  736. modalProgressBar.style.width = '0%';
  737. }
  738. }
  739. // Next pattern - clean up to show only filename
  740. const modalNextPattern = document.getElementById('modal-next-pattern');
  741. if (modalNextPattern) {
  742. if (status.playlist && status.playlist.next_file) {
  743. modalNextPattern.textContent = getCleanPatternName(status.playlist.next_file);
  744. } else {
  745. modalNextPattern.textContent = 'None';
  746. }
  747. }
  748. // Pause button
  749. const modalPauseBtn = document.getElementById('modal-pause-button');
  750. if (modalPauseBtn) {
  751. const pauseIcon = modalPauseBtn.querySelector('.material-icons');
  752. if (status.is_paused) {
  753. pauseIcon.textContent = 'play_arrow';
  754. } else {
  755. pauseIcon.textContent = 'pause';
  756. }
  757. }
  758. // Skip button visibility
  759. const modalSkipBtn = document.getElementById('modal-skip-button');
  760. if (modalSkipBtn) {
  761. if (status.playlist && status.playlist.next_file) {
  762. modalSkipBtn.classList.remove('invisible');
  763. } else {
  764. modalSkipBtn.classList.add('invisible');
  765. }
  766. }
  767. // Speed display
  768. const modalSpeedDisplay = document.getElementById('modal-speed-display');
  769. if (modalSpeedDisplay && status.speed && !isEditingSpeed) {
  770. modalSpeedDisplay.textContent = status.speed;
  771. }
  772. }
  773. // Toggle modal visibility
  774. function togglePreviewModal() {
  775. const modal = document.getElementById('playerPreviewModal');
  776. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  777. if (!modal || !toggleBtn) return;
  778. const isHidden = modal.classList.contains('hidden');
  779. if (isHidden) {
  780. openPlayerPreviewModal();
  781. } else {
  782. setModalVisibility(false, true);
  783. toggleBtn.classList.remove('active-tab');
  784. toggleBtn.classList.add('inactive-tab');
  785. }
  786. }
  787. // Button event handlers
  788. document.addEventListener('DOMContentLoaded', async () => {
  789. try {
  790. // Initialize WebSocket connection
  791. initializeWebSocket();
  792. // Setup player preview modal events
  793. setupPlayerPreviewModalEvents();
  794. console.log('Player initialized successfully');
  795. } catch (error) {
  796. console.error(`Error during initialization: ${error.message}`);
  797. }
  798. });
  799. // Initialize WebSocket connection
  800. function initializeWebSocket() {
  801. connectWebSocket();
  802. }
  803. // Clean up WebSocket when page unloads to prevent memory leaks
  804. window.addEventListener('beforeunload', () => {
  805. if (ws) {
  806. // Disable reconnection before closing
  807. ws.onclose = null;
  808. ws.close();
  809. ws = null;
  810. }
  811. });
  812. // Add resize handler for responsive canvas with debouncing
  813. let resizeTimeout;
  814. window.addEventListener('resize', () => {
  815. const canvas = document.getElementById('playerPreviewCanvas');
  816. const modal = document.getElementById('playerPreviewModal');
  817. if (canvas && modal && !modal.classList.contains('hidden')) {
  818. // Clear previous timeout
  819. clearTimeout(resizeTimeout);
  820. // Debounce resize calls to avoid excessive updates
  821. resizeTimeout = setTimeout(() => {
  822. const ctx = canvas.getContext('2d');
  823. setupPlayerPreviewCanvas(ctx);
  824. drawPlayerPreview(ctx, targetProgress);
  825. }, 16); // ~60fps update rate
  826. }
  827. });
  828. // Handle file changes and reload preview data
  829. function handleFileChange(newFile) {
  830. if (newFile !== currentPreviewFile) {
  831. currentPreviewFile = newFile;
  832. if (newFile) {
  833. loadPlayerPreviewData(`./patterns/${newFile}`);
  834. } else {
  835. playerPreviewData = null;
  836. }
  837. }
  838. }
  839. // Cache All Previews Prompt functionality
  840. let cacheAllInProgress = false;
  841. function shouldShowCacheAllPrompt() {
  842. // Check if we've already shown the prompt
  843. const promptShown = localStorage.getItem('cacheAllPromptShown');
  844. console.log('shouldShowCacheAllPrompt - promptShown:', promptShown);
  845. return !promptShown;
  846. }
  847. function showCacheAllPrompt(forceShow = false) {
  848. console.log('showCacheAllPrompt called, forceShow:', forceShow);
  849. if (!forceShow && !shouldShowCacheAllPrompt()) {
  850. console.log('Cache all prompt already shown, skipping');
  851. return;
  852. }
  853. const modal = document.getElementById('cacheAllPromptModal');
  854. if (modal) {
  855. console.log('Showing cache all prompt modal');
  856. modal.classList.remove('hidden');
  857. // Store whether this was forced (manually triggered)
  858. modal.dataset.manuallyTriggered = forceShow.toString();
  859. } else {
  860. console.log('Cache all prompt modal not found');
  861. }
  862. }
  863. function hideCacheAllPrompt() {
  864. const modal = document.getElementById('cacheAllPromptModal');
  865. if (modal) {
  866. modal.classList.add('hidden');
  867. }
  868. }
  869. function markCacheAllPromptAsShown() {
  870. localStorage.setItem('cacheAllPromptShown', 'true');
  871. }
  872. function initializeCacheAllPrompt() {
  873. const modal = document.getElementById('cacheAllPromptModal');
  874. const skipBtn = document.getElementById('skipCacheAllBtn');
  875. const startBtn = document.getElementById('startCacheAllBtn');
  876. const closeBtn = document.getElementById('closeCacheAllBtn');
  877. if (!modal || !skipBtn || !startBtn || !closeBtn) {
  878. return;
  879. }
  880. // Skip button handler
  881. skipBtn.addEventListener('click', () => {
  882. const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
  883. hideCacheAllPrompt();
  884. // Only mark as shown if it was automatically shown (not manually triggered)
  885. if (!wasManuallyTriggered) {
  886. markCacheAllPromptAsShown();
  887. }
  888. });
  889. // Close button handler (after completion)
  890. closeBtn.addEventListener('click', () => {
  891. const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
  892. hideCacheAllPrompt();
  893. // Always mark as shown after successful completion
  894. if (!wasManuallyTriggered) {
  895. markCacheAllPromptAsShown();
  896. }
  897. });
  898. // Start caching button handler
  899. startBtn.addEventListener('click', async () => {
  900. if (cacheAllInProgress) {
  901. return;
  902. }
  903. cacheAllInProgress = true;
  904. // Hide buttons and show progress
  905. document.getElementById('cacheAllButtons').classList.add('hidden');
  906. document.getElementById('cacheAllProgress').classList.remove('hidden');
  907. try {
  908. await startCacheAllProcess();
  909. // Show completion message
  910. document.getElementById('cacheAllProgress').classList.add('hidden');
  911. document.getElementById('cacheAllComplete').classList.remove('hidden');
  912. } catch (error) {
  913. console.error('Error caching all previews:', error);
  914. // Show error and reset
  915. document.getElementById('cacheAllProgressText').textContent = 'Error occurred during caching';
  916. setTimeout(() => {
  917. hideCacheAllPrompt();
  918. markCacheAllPromptAsShown();
  919. }, 3000);
  920. } finally {
  921. cacheAllInProgress = false;
  922. }
  923. });
  924. }
  925. async function startCacheAllProcess() {
  926. try {
  927. // Get list of patterns using cached function
  928. const patterns = await getCachedPatternFiles();
  929. if (!patterns || patterns.length === 0) {
  930. throw new Error('No patterns found');
  931. }
  932. const progressBar = document.getElementById('cacheAllProgressBar');
  933. const progressText = document.getElementById('cacheAllProgressText');
  934. const progressPercentage = document.getElementById('cacheAllProgressPercentage');
  935. let completed = 0;
  936. const batchSize = 5; // Process in small batches to avoid overwhelming the server
  937. for (let i = 0; i < patterns.length; i += batchSize) {
  938. const batch = patterns.slice(i, i + batchSize);
  939. // Update progress text
  940. progressText.textContent = `Caching previews... (${Math.min(i + batchSize, patterns.length)}/${patterns.length})`;
  941. // Process batch
  942. const batchPromises = batch.map(async (pattern) => {
  943. try {
  944. const previewResponse = await fetch('/preview_thr', {
  945. method: 'POST',
  946. headers: {
  947. 'Content-Type': 'application/json',
  948. },
  949. body: JSON.stringify({ file_name: pattern })
  950. });
  951. if (previewResponse.ok) {
  952. const data = await previewResponse.json();
  953. if (data.preview_url) {
  954. // Pre-load the image to cache it
  955. return new Promise((resolve) => {
  956. const img = new Image();
  957. img.onload = () => resolve();
  958. img.onerror = () => resolve(); // Continue even if image fails
  959. img.src = data.preview_url;
  960. });
  961. }
  962. }
  963. return Promise.resolve();
  964. } catch (error) {
  965. console.warn(`Failed to cache preview for ${pattern}:`, error);
  966. return Promise.resolve(); // Continue with other patterns
  967. }
  968. });
  969. await Promise.all(batchPromises);
  970. completed += batch.length;
  971. // Update progress bar
  972. const progress = Math.round((completed / patterns.length) * 100);
  973. progressBar.style.width = `${progress}%`;
  974. progressPercentage.textContent = `${progress}%`;
  975. // Small delay between batches to prevent overwhelming the server
  976. if (i + batchSize < patterns.length) {
  977. await new Promise(resolve => setTimeout(resolve, 100));
  978. }
  979. }
  980. progressText.textContent = `Completed! Cached ${patterns.length} previews.`;
  981. } catch (error) {
  982. throw error;
  983. }
  984. }
  985. // Function to be called after initial cache generation completes
  986. function onInitialCacheComplete() {
  987. console.log('onInitialCacheComplete called');
  988. // Show the cache all prompt after a short delay
  989. setTimeout(() => {
  990. console.log('Triggering cache all prompt after delay');
  991. showCacheAllPrompt();
  992. }, 1000);
  993. }
  994. // Initialize on DOM load
  995. document.addEventListener('DOMContentLoaded', () => {
  996. initializeCacheAllPrompt();
  997. });
  998. // Make functions available globally for debugging
  999. window.onInitialCacheComplete = onInitialCacheComplete;
  1000. window.showCacheAllPrompt = showCacheAllPrompt;
  1001. window.testCacheAllPrompt = function() {
  1002. console.log('Manual test trigger');
  1003. // Clear localStorage for testing
  1004. localStorage.removeItem('cacheAllPromptShown');
  1005. showCacheAllPrompt();
  1006. };