base.js 35 KB

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