1
0

base.js 36 KB

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