1
0

base.js 38 KB

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