base.js 36 KB

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