base.js 43 KB

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