base.js 36 KB

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