1
0

base.js 40 KB

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