1
0

base.js 43 KB

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