base.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  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;
  160. const modal = document.getElementById('playerPreviewModal');
  161. if (!container || !modal) return;
  162. // Get the modal's available height for the canvas area
  163. const modalContent = modal.querySelector('.bg-white');
  164. const modalHeader = modal.querySelector('.flex-shrink-0');
  165. const modalControls = modal.querySelector('.flex-shrink-0:last-child');
  166. const modalHeight = modalContent.clientHeight;
  167. const headerHeight = modalHeader ? modalHeader.clientHeight : 0;
  168. const controlsHeight = modalControls ? modalControls.clientHeight : 0;
  169. const padding = 32; // 2 * p-4 (16px each)
  170. // Calculate available height for canvas
  171. const availableHeight = modalHeight - headerHeight - controlsHeight - padding;
  172. // Calculate the size (use the smaller of width or height to maintain aspect ratio)
  173. const containerWidth = container.clientWidth;
  174. const containerHeight = container.clientHeight;
  175. const size = Math.min(containerWidth, containerHeight, availableHeight);
  176. // Set the internal canvas size for high-DPI rendering
  177. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  178. canvas.width = size * pixelRatio;
  179. canvas.height = size * pixelRatio;
  180. // Set the display size
  181. canvas.style.width = `${size}px`;
  182. canvas.style.height = `${size}px`;
  183. }
  184. // Get interpolated coordinate at specific progress
  185. function getInterpolatedCoordinate(progress) {
  186. if (!playerPreviewData || playerPreviewData.length === 0) return null;
  187. const totalPoints = playerPreviewData.length;
  188. const exactIndex = progress * (totalPoints - 1);
  189. const index = Math.floor(exactIndex);
  190. const fraction = exactIndex - index;
  191. // Ensure we don't go out of bounds
  192. if (index >= totalPoints - 1) {
  193. return playerPreviewData[totalPoints - 1];
  194. }
  195. if (index < 0) {
  196. return playerPreviewData[0];
  197. }
  198. // Get the two coordinates to interpolate between
  199. const [theta1, rho1] = playerPreviewData[index];
  200. const [theta2, rho2] = playerPreviewData[index + 1];
  201. // Interpolate theta (handle angle wrapping)
  202. let deltaTheta = theta2 - theta1;
  203. if (deltaTheta > Math.PI) deltaTheta -= 2 * Math.PI;
  204. if (deltaTheta < -Math.PI) deltaTheta += 2 * Math.PI;
  205. const interpolatedTheta = theta1 + deltaTheta * fraction;
  206. const interpolatedRho = rho1 + (rho2 - rho1) * fraction;
  207. return [interpolatedTheta, interpolatedRho];
  208. }
  209. // Draw player preview for modal
  210. function drawPlayerPreview(ctx, progress) {
  211. if (!ctx || !playerPreviewData || playerPreviewData.length === 0) return;
  212. const canvas = ctx.canvas;
  213. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  214. const containerSize = canvas.width / pixelRatio;
  215. const center = containerSize / 2;
  216. const scale = (containerSize / 2) - 30;
  217. ctx.save();
  218. // Clear canvas for fresh drawing
  219. ctx.clearRect(0, 0, canvas.width, canvas.height);
  220. // Create circular clipping path
  221. ctx.beginPath();
  222. ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
  223. ctx.clip();
  224. // Setup coordinate system for drawing
  225. ctx.scale(pixelRatio, pixelRatio);
  226. // Calculate how many points to draw
  227. const totalPoints = playerPreviewData.length;
  228. const pointsToDraw = Math.floor(totalPoints * progress);
  229. if (pointsToDraw < 2) {
  230. ctx.restore();
  231. return;
  232. }
  233. // Draw the pattern
  234. ctx.beginPath();
  235. ctx.strokeStyle = '#808080';
  236. ctx.lineWidth = 1;
  237. ctx.lineCap = 'round';
  238. ctx.lineJoin = 'round';
  239. // Enable high quality rendering
  240. ctx.imageSmoothingEnabled = true;
  241. ctx.imageSmoothingQuality = 'high';
  242. // Draw pattern lines up to the last complete segment
  243. for (let i = 0; i < pointsToDraw - 1; i++) {
  244. const [theta1, rho1] = playerPreviewData[i];
  245. const [theta2, rho2] = playerPreviewData[i + 1];
  246. const x1 = center + rho1 * scale * Math.cos(theta1);
  247. const y1 = center + rho1 * scale * Math.sin(theta1);
  248. const x2 = center + rho2 * scale * Math.cos(theta2);
  249. const y2 = center + rho2 * scale * Math.sin(theta2);
  250. if (i === 0) {
  251. ctx.moveTo(x1, y1);
  252. }
  253. ctx.lineTo(x2, y2);
  254. }
  255. // Draw the final partial segment to the interpolated position
  256. if (pointsToDraw > 0) {
  257. const interpolatedCoord = getInterpolatedCoordinate(progress);
  258. if (interpolatedCoord && pointsToDraw > 1) {
  259. // Get the last complete coordinate
  260. const [lastTheta, lastRho] = playerPreviewData[pointsToDraw - 1];
  261. const lastX = center + lastRho * scale * Math.cos(lastTheta);
  262. const lastY = center + lastRho * scale * Math.sin(lastTheta);
  263. // Draw line to interpolated position
  264. const [interpTheta, interpRho] = interpolatedCoord;
  265. const interpX = center + interpRho * scale * Math.cos(interpTheta);
  266. const interpY = center + interpRho * scale * Math.sin(interpTheta);
  267. ctx.lineTo(interpX, interpY);
  268. }
  269. }
  270. ctx.stroke();
  271. // Draw current position dot with interpolated position
  272. if (pointsToDraw > 0) {
  273. const interpolatedCoord = getInterpolatedCoordinate(progress);
  274. if (interpolatedCoord) {
  275. const [theta, rho] = interpolatedCoord;
  276. const x = center + rho * scale * Math.cos(theta);
  277. const y = center + rho * scale * Math.sin(theta);
  278. // Draw white border
  279. ctx.beginPath();
  280. ctx.fillStyle = '#ffffff';
  281. ctx.arc(x, y, 7.5, 0, Math.PI * 2);
  282. ctx.fill();
  283. // Draw red dot
  284. ctx.beginPath();
  285. ctx.fillStyle = '#ff0000';
  286. ctx.arc(x, y, 6, 0, Math.PI * 2);
  287. ctx.fill();
  288. }
  289. }
  290. ctx.restore();
  291. }
  292. // Load pattern coordinates for player preview
  293. async function loadPlayerPreviewData(pattern) {
  294. try {
  295. const response = await fetch('/get_theta_rho_coordinates', {
  296. method: 'POST',
  297. headers: { 'Content-Type': 'application/json' },
  298. body: JSON.stringify({ file_name: pattern })
  299. });
  300. if (!response.ok) {
  301. throw new Error(`HTTP error! status: ${response.status}`);
  302. }
  303. const data = await response.json();
  304. if (data.error) {
  305. throw new Error(data.error);
  306. }
  307. playerPreviewData = data.coordinates;
  308. // Store the filename for comparison
  309. playerPreviewData.fileName = pattern.replace('./patterns/', '');
  310. } catch (error) {
  311. console.error(`Error loading player preview data: ${error.message}`);
  312. playerPreviewData = null;
  313. }
  314. }
  315. // Ultra-smooth animation function for modal
  316. function animateModalPreview() {
  317. const modal = document.getElementById('playerPreviewModal');
  318. if (!modal || modal.classList.contains('hidden')) return;
  319. const canvas = document.getElementById('playerPreviewCanvas');
  320. const ctx = canvas.getContext('2d');
  321. if (!ctx || !playerPreviewData) return;
  322. const currentTime = Date.now();
  323. const elapsed = currentTime - modalAnimationStartTime;
  324. const totalDuration = animationDuration;
  325. // Calculate smooth progress with easing
  326. const rawProgress = Math.min(elapsed / totalDuration, 1);
  327. const easeProgress = rawProgress < 0.5
  328. ? 2 * rawProgress * rawProgress
  329. : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2;
  330. // Interpolate between last and target progress
  331. const currentProgress = modalLastProgress + (modalTargetProgress - modalLastProgress) * easeProgress;
  332. // Draw the pattern up to current progress
  333. drawPlayerPreview(ctx, currentProgress);
  334. // Continue animation if not at target
  335. if (rawProgress < 1) {
  336. modalAnimationId = requestAnimationFrame(animateModalPreview);
  337. }
  338. }
  339. // Update modal preview with smooth animation
  340. function updateModalPreviewSmooth(newProgress) {
  341. if (newProgress === modalTargetProgress) return; // No change needed
  342. // Stop any existing animation
  343. if (modalAnimationId) {
  344. cancelAnimationFrame(modalAnimationId);
  345. }
  346. // Update animation parameters
  347. modalLastProgress = modalTargetProgress;
  348. modalTargetProgress = newProgress;
  349. modalAnimationStartTime = Date.now();
  350. // Start smooth animation
  351. animateModalPreview();
  352. }
  353. // Setup player preview modal events
  354. function setupPlayerPreviewModalEvents() {
  355. const modal = document.getElementById('playerPreviewModal');
  356. const closeBtn = document.getElementById('closePlayerPreview');
  357. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  358. if (!modal || !closeBtn || !toggleBtn) return;
  359. // Remove any existing event listeners to prevent conflicts
  360. const newToggleBtn = toggleBtn.cloneNode(true);
  361. toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
  362. // Toggle button click handler
  363. newToggleBtn.addEventListener('click', () => {
  364. const isHidden = modal.classList.contains('hidden');
  365. if (isHidden) {
  366. openPlayerPreviewModal();
  367. } else {
  368. modal.classList.add('hidden');
  369. }
  370. });
  371. // Close modal when clicking close button
  372. closeBtn.addEventListener('click', () => {
  373. modal.classList.add('hidden');
  374. });
  375. // Close modal when clicking outside
  376. modal.addEventListener('click', (e) => {
  377. if (e.target === modal) {
  378. modal.classList.add('hidden');
  379. }
  380. });
  381. // Close modal with Escape key
  382. document.addEventListener('keydown', (e) => {
  383. if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
  384. modal.classList.add('hidden');
  385. }
  386. });
  387. // Setup modal control buttons
  388. setupModalControls();
  389. }
  390. // Handle pause/resume toggle
  391. async function togglePauseResume() {
  392. const pauseButton = document.getElementById('modal-pause-button');
  393. if (!pauseButton) return;
  394. try {
  395. const pauseIcon = pauseButton.querySelector('.material-icons');
  396. const isCurrentlyPaused = pauseIcon.textContent === 'play_arrow';
  397. const endpoint = isCurrentlyPaused ? '/resume_execution' : '/pause_execution';
  398. const response = await fetch(endpoint, { method: 'POST' });
  399. if (!response.ok) throw new Error(`Failed to ${isCurrentlyPaused ? 'resume' : 'pause'}`);
  400. } catch (error) {
  401. console.error('Error toggling pause:', error);
  402. showStatusMessage('Failed to pause/resume pattern', 'error');
  403. }
  404. }
  405. // Setup modal controls
  406. function setupModalControls() {
  407. const pauseButton = document.getElementById('modal-pause-button');
  408. const skipButton = document.getElementById('modal-skip-button');
  409. const stopButton = document.getElementById('modal-stop-button');
  410. const speedDisplay = document.getElementById('modal-speed-display');
  411. const speedInput = document.getElementById('modal-speed-input');
  412. if (!pauseButton || !skipButton || !stopButton || !speedDisplay || !speedInput) return;
  413. // Pause button click handler
  414. pauseButton.addEventListener('click', togglePauseResume);
  415. // Skip button click handler
  416. skipButton.addEventListener('click', async () => {
  417. try {
  418. const response = await fetch('/skip_pattern', { method: 'POST' });
  419. if (!response.ok) throw new Error('Failed to skip pattern');
  420. } catch (error) {
  421. console.error('Error skipping pattern:', error);
  422. showStatusMessage('Failed to skip pattern', 'error');
  423. }
  424. });
  425. // Stop button click handler
  426. stopButton.addEventListener('click', async () => {
  427. try {
  428. const response = await fetch('/stop_execution', { method: 'POST' });
  429. if (!response.ok) throw new Error('Failed to stop pattern');
  430. else {
  431. // Hide modal when stopping
  432. const modal = document.getElementById('playerPreviewModal');
  433. if (modal) modal.classList.add('hidden');
  434. }
  435. } catch (error) {
  436. console.error('Error stopping pattern:', error);
  437. showStatusMessage('Failed to stop pattern', 'error');
  438. }
  439. });
  440. // Speed display click to edit
  441. speedDisplay.addEventListener('click', () => {
  442. isEditingSpeed = true;
  443. speedDisplay.classList.add('hidden');
  444. speedInput.value = speedDisplay.textContent;
  445. speedInput.classList.remove('hidden');
  446. speedInput.focus();
  447. speedInput.select();
  448. });
  449. // Speed input handlers
  450. speedInput.addEventListener('keydown', (e) => {
  451. if (e.key === 'Enter') {
  452. e.preventDefault();
  453. exitModalSpeedEditMode(true);
  454. } else if (e.key === 'Escape') {
  455. e.preventDefault();
  456. exitModalSpeedEditMode(false);
  457. }
  458. });
  459. speedInput.addEventListener('blur', () => {
  460. exitModalSpeedEditMode(true);
  461. });
  462. }
  463. // Exit modal speed edit mode
  464. async function exitModalSpeedEditMode(save = false) {
  465. const speedDisplay = document.getElementById('modal-speed-display');
  466. const speedInput = document.getElementById('modal-speed-input');
  467. if (!speedDisplay || !speedInput) return;
  468. isEditingSpeed = false;
  469. speedInput.classList.add('hidden');
  470. speedDisplay.classList.remove('hidden');
  471. if (save) {
  472. const speed = parseInt(speedInput.value);
  473. if (!isNaN(speed) && speed >= 1 && speed <= 5000) {
  474. try {
  475. const response = await fetch('/set_speed', {
  476. method: 'POST',
  477. headers: { 'Content-Type': 'application/json' },
  478. body: JSON.stringify({ speed: speed })
  479. });
  480. const data = await response.json();
  481. if (data.success) {
  482. speedDisplay.textContent = speed;
  483. showStatusMessage(`Speed set to ${speed} mm/s`, 'success');
  484. } else {
  485. throw new Error(data.detail || 'Failed to set speed');
  486. }
  487. } catch (error) {
  488. console.error('Error setting speed:', error);
  489. showStatusMessage('Failed to set speed', 'error');
  490. }
  491. } else {
  492. showStatusMessage('Please enter a valid speed (1-5000)', 'error');
  493. }
  494. }
  495. }
  496. // Helper function to clean up pattern names
  497. function getCleanPatternName(filePath) {
  498. if (!filePath) return '';
  499. const fileName = filePath.replace('./patterns/', '');
  500. return fileName.split('/').pop().replace('.thr', '');
  501. }
  502. // Sync modal controls with player status
  503. function syncModalControls(status) {
  504. // Pattern name - clean up to show only filename
  505. const modalPatternName = document.getElementById('modal-pattern-name');
  506. if (modalPatternName && status.current_file) {
  507. modalPatternName.textContent = getCleanPatternName(status.current_file);
  508. }
  509. // Pattern preview image
  510. const modalPatternPreviewImg = document.getElementById('modal-pattern-preview-img');
  511. if (modalPatternPreviewImg && status.current_file) {
  512. const encodedFilename = status.current_file.replace('./patterns/', '').replace(/\//g, '--');
  513. const previewUrl = `/preview/${encodedFilename}`;
  514. modalPatternPreviewImg.src = previewUrl;
  515. }
  516. // ETA
  517. const modalEta = document.getElementById('modal-eta');
  518. if (modalEta) {
  519. if (status.progress && status.progress.remaining_time !== null) {
  520. const minutes = Math.floor(status.progress.remaining_time / 60);
  521. const seconds = Math.floor(status.progress.remaining_time % 60);
  522. modalEta.textContent = `ETA: ${minutes}:${seconds.toString().padStart(2, '0')}`;
  523. } else {
  524. modalEta.textContent = 'ETA: calculating...';
  525. }
  526. }
  527. // Progress bar
  528. const modalProgressBar = document.getElementById('modal-progress-bar');
  529. if (modalProgressBar) {
  530. if (status.progress && status.progress.percentage !== null) {
  531. modalProgressBar.style.width = `${status.progress.percentage}%`;
  532. } else {
  533. modalProgressBar.style.width = '0%';
  534. }
  535. }
  536. // Next pattern - clean up to show only filename
  537. const modalNextPattern = document.getElementById('modal-next-pattern');
  538. if (modalNextPattern) {
  539. if (status.playlist && status.playlist.next_file) {
  540. modalNextPattern.textContent = getCleanPatternName(status.playlist.next_file);
  541. } else {
  542. modalNextPattern.textContent = 'None';
  543. }
  544. }
  545. // Pause button
  546. const modalPauseBtn = document.getElementById('modal-pause-button');
  547. if (modalPauseBtn) {
  548. const pauseIcon = modalPauseBtn.querySelector('.material-icons');
  549. if (status.is_paused) {
  550. pauseIcon.textContent = 'play_arrow';
  551. } else {
  552. pauseIcon.textContent = 'pause';
  553. }
  554. }
  555. // Skip button visibility
  556. const modalSkipBtn = document.getElementById('modal-skip-button');
  557. if (modalSkipBtn) {
  558. if (status.playlist && status.playlist.next_file) {
  559. modalSkipBtn.classList.remove('invisible');
  560. } else {
  561. modalSkipBtn.classList.add('invisible');
  562. }
  563. }
  564. // Speed display
  565. const modalSpeedDisplay = document.getElementById('modal-speed-display');
  566. if (modalSpeedDisplay && status.speed && !isEditingSpeed) {
  567. modalSpeedDisplay.textContent = status.speed;
  568. }
  569. }
  570. // Toggle modal visibility
  571. function togglePreviewModal() {
  572. const modal = document.getElementById('playerPreviewModal');
  573. const toggleBtn = document.getElementById('toggle-preview-modal-btn');
  574. if (!modal || !toggleBtn) return;
  575. const isHidden = modal.classList.contains('hidden');
  576. if (isHidden) {
  577. openPlayerPreviewModal();
  578. } else {
  579. modal.classList.add('hidden');
  580. toggleBtn.classList.remove('active-tab');
  581. toggleBtn.classList.add('inactive-tab');
  582. }
  583. }
  584. // Button event handlers
  585. document.addEventListener('DOMContentLoaded', async () => {
  586. try {
  587. // Initialize WebSocket connection
  588. initializeWebSocket();
  589. // Setup player preview modal events
  590. setupPlayerPreviewModalEvents();
  591. console.log('Player initialized successfully');
  592. } catch (error) {
  593. console.error(`Error during initialization: ${error.message}`);
  594. }
  595. });
  596. // Initialize WebSocket connection
  597. function initializeWebSocket() {
  598. connectWebSocket();
  599. }
  600. // Add resize handler for responsive canvas
  601. window.addEventListener('resize', () => {
  602. const canvas = document.getElementById('playerPreviewCanvas');
  603. const modal = document.getElementById('playerPreviewModal');
  604. if (canvas && modal && !modal.classList.contains('hidden')) {
  605. const ctx = canvas.getContext('2d');
  606. setupPlayerPreviewCanvas(ctx);
  607. drawPlayerPreview(ctx, targetProgress);
  608. }
  609. });
  610. // Handle file changes and reload preview data
  611. function handleFileChange(newFile) {
  612. if (newFile !== currentPreviewFile) {
  613. currentPreviewFile = newFile;
  614. if (newFile) {
  615. loadPlayerPreviewData(`./patterns/${newFile}`);
  616. } else {
  617. playerPreviewData = null;
  618. }
  619. }
  620. }