base.js 35 KB

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