index.js 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870
  1. // Global variables
  2. let allPatterns = [];
  3. // Helper function to normalize file paths for cross-platform compatibility
  4. function normalizeFilePath(filePath) {
  5. if (!filePath) return '';
  6. return filePath.replace('./patterns\\', '').replace('./patterns/', '').replace('patterns\\', '').replace('patterns/', '');
  7. }
  8. let selectedPattern = null;
  9. let previewObserver = null;
  10. let currentBatch = 0;
  11. const BATCH_SIZE = 40; // Increased batch size for better performance
  12. let previewCache = new Map(); // Simple in-memory cache for preview data
  13. let imageCache = new Map(); // Cache for preloaded images
  14. // Global variables for lazy loading
  15. let pendingPatterns = new Map(); // pattern -> element mapping
  16. let batchTimeout = null;
  17. const INITIAL_BATCH_SIZE = 12; // Smaller initial batch for faster first load
  18. const LAZY_BATCH_SIZE = 5; // Reduced batch size for smoother loading
  19. const MAX_RETRIES = 3; // Maximum number of retries for failed loads
  20. const RETRY_DELAY = 1000; // Delay between retries in ms
  21. // Shared caching for patterns list (persistent across sessions)
  22. const PATTERNS_CACHE_KEY = 'dune_weaver_patterns_cache';
  23. // IndexedDB cache for preview images with size management (shared with playlists page)
  24. const PREVIEW_CACHE_DB_NAME = 'dune_weaver_previews';
  25. const PREVIEW_CACHE_DB_VERSION = 1;
  26. const PREVIEW_CACHE_STORE_NAME = 'previews';
  27. const MAX_CACHE_SIZE_MB = 200;
  28. const MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_MB * 1024 * 1024;
  29. let previewCacheDB = null;
  30. // Define constants for log message types
  31. const LOG_TYPE = {
  32. SUCCESS: 'success',
  33. WARNING: 'warning',
  34. ERROR: 'error',
  35. INFO: 'info',
  36. DEBUG: 'debug'
  37. };
  38. // Cache progress storage keys
  39. const CACHE_PROGRESS_KEY = 'dune_weaver_cache_progress';
  40. const CACHE_TIMESTAMP_KEY = 'dune_weaver_cache_timestamp';
  41. const CACHE_PROGRESS_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
  42. // Animated Preview Variables
  43. let animatedPreviewData = null;
  44. let animationFrameId = null;
  45. let isPlaying = false;
  46. let currentProgress = 0;
  47. let animationSpeed = 1;
  48. let lastTimestamp = 0;
  49. // Function to show status message
  50. function showStatusMessage(message, type = 'success') {
  51. const statusContainer = document.getElementById('status-message-container');
  52. const statusMessage = document.getElementById('status-message');
  53. if (!statusContainer || !statusMessage) return;
  54. // Set message and color based on type
  55. statusMessage.textContent = message;
  56. statusMessage.className = `text-base font-semibold opacity-0 transform -translate-y-2 transition-all duration-300 ease-in-out px-4 py-2 rounded-lg shadow-lg ${
  57. type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' :
  58. type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' :
  59. type === 'warning' ? 'bg-yellow-50 text-yellow-700 border border-yellow-200' :
  60. 'bg-blue-50 text-blue-700 border border-blue-200'
  61. }`;
  62. // Show message with animation
  63. requestAnimationFrame(() => {
  64. statusMessage.classList.remove('opacity-0', '-translate-y-2');
  65. statusMessage.classList.add('opacity-100', 'translate-y-0');
  66. });
  67. // Hide message after 5 seconds
  68. setTimeout(() => {
  69. statusMessage.classList.remove('opacity-100', 'translate-y-0');
  70. statusMessage.classList.add('opacity-0', '-translate-y-2');
  71. }, 5000);
  72. }
  73. // Function to log messages
  74. function logMessage(message, type = LOG_TYPE.DEBUG) {
  75. console.log(`[${type}] ${message}`);
  76. }
  77. // Initialize IndexedDB for preview caching (shared with playlists page)
  78. async function initPreviewCacheDB() {
  79. if (previewCacheDB) return previewCacheDB;
  80. return new Promise((resolve, reject) => {
  81. const request = indexedDB.open(PREVIEW_CACHE_DB_NAME, PREVIEW_CACHE_DB_VERSION);
  82. request.onerror = () => {
  83. logMessage('Failed to open preview cache database', LOG_TYPE.ERROR);
  84. reject(request.error);
  85. };
  86. request.onsuccess = () => {
  87. previewCacheDB = request.result;
  88. logMessage('Preview cache database opened successfully', LOG_TYPE.DEBUG);
  89. resolve(previewCacheDB);
  90. };
  91. request.onupgradeneeded = (event) => {
  92. const db = event.target.result;
  93. // Create object store for preview cache
  94. const store = db.createObjectStore(PREVIEW_CACHE_STORE_NAME, { keyPath: 'pattern' });
  95. store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
  96. store.createIndex('size', 'size', { unique: false });
  97. logMessage('Preview cache database schema created', LOG_TYPE.DEBUG);
  98. };
  99. });
  100. }
  101. // Get preview from IndexedDB cache
  102. async function getPreviewFromCache(pattern) {
  103. try {
  104. if (!previewCacheDB) await initPreviewCacheDB();
  105. const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
  106. const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
  107. return new Promise((resolve, reject) => {
  108. const request = store.get(pattern);
  109. request.onsuccess = () => {
  110. const result = request.result;
  111. if (result) {
  112. // Update last accessed time
  113. result.lastAccessed = Date.now();
  114. store.put(result);
  115. resolve(result.data);
  116. } else {
  117. resolve(null);
  118. }
  119. };
  120. request.onerror = () => reject(request.error);
  121. });
  122. } catch (error) {
  123. logMessage(`Error getting preview from cache: ${error.message}`, LOG_TYPE.WARNING);
  124. return null;
  125. }
  126. }
  127. // Clear a specific pattern from IndexedDB cache
  128. async function clearPatternFromIndexedDB(pattern) {
  129. try {
  130. if (!previewCacheDB) await initPreviewCacheDB();
  131. const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
  132. const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
  133. await new Promise((resolve, reject) => {
  134. const deleteRequest = store.delete(pattern);
  135. deleteRequest.onsuccess = () => {
  136. logMessage(`Cleared ${pattern} from IndexedDB cache`, LOG_TYPE.DEBUG);
  137. resolve();
  138. };
  139. deleteRequest.onerror = () => {
  140. logMessage(`Failed to clear ${pattern} from IndexedDB: ${deleteRequest.error}`, LOG_TYPE.WARNING);
  141. reject(deleteRequest.error);
  142. };
  143. });
  144. } catch (error) {
  145. logMessage(`Error clearing pattern from IndexedDB: ${error.message}`, LOG_TYPE.WARNING);
  146. }
  147. }
  148. // Save preview to IndexedDB cache with size management
  149. async function savePreviewToCache(pattern, previewData) {
  150. try {
  151. if (!previewCacheDB) await initPreviewCacheDB();
  152. // Validate preview data before attempting to fetch
  153. if (!previewData || !previewData.image_data) {
  154. logMessage(`Invalid preview data for ${pattern}, skipping cache save`, LOG_TYPE.WARNING);
  155. return;
  156. }
  157. // Convert preview URL to blob for size calculation
  158. const response = await fetch(previewData.image_data);
  159. const blob = await response.blob();
  160. const size = blob.size;
  161. // Check if we need to free up space
  162. await managePreviewCacheSize(size);
  163. const cacheEntry = {
  164. pattern: pattern,
  165. data: previewData,
  166. size: size,
  167. lastAccessed: Date.now(),
  168. created: Date.now()
  169. };
  170. const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
  171. const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
  172. return new Promise((resolve, reject) => {
  173. const request = store.put(cacheEntry);
  174. request.onsuccess = () => {
  175. logMessage(`Preview cached for ${pattern} (${(size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
  176. resolve();
  177. };
  178. request.onerror = () => reject(request.error);
  179. });
  180. } catch (error) {
  181. logMessage(`Error saving preview to cache: ${error.message}`, LOG_TYPE.WARNING);
  182. }
  183. }
  184. // Manage cache size by removing least recently used items
  185. async function managePreviewCacheSize(newItemSize) {
  186. try {
  187. const currentSize = await getPreviewCacheSize();
  188. if (currentSize + newItemSize <= MAX_CACHE_SIZE_BYTES) {
  189. return; // No cleanup needed
  190. }
  191. logMessage(`Cache size would exceed limit (${((currentSize + newItemSize) / 1024 / 1024).toFixed(1)}MB), cleaning up...`, LOG_TYPE.DEBUG);
  192. const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
  193. const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
  194. const index = store.index('lastAccessed');
  195. // Get all entries sorted by last accessed (oldest first)
  196. const entries = await new Promise((resolve, reject) => {
  197. const request = index.getAll();
  198. request.onsuccess = () => resolve(request.result);
  199. request.onerror = () => reject(request.error);
  200. });
  201. // Sort by last accessed time (oldest first)
  202. entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
  203. let freedSpace = 0;
  204. const targetSpace = newItemSize + (MAX_CACHE_SIZE_BYTES * 0.1); // Free 10% extra buffer
  205. for (const entry of entries) {
  206. if (freedSpace >= targetSpace) break;
  207. await new Promise((resolve, reject) => {
  208. const deleteRequest = store.delete(entry.pattern);
  209. deleteRequest.onsuccess = () => {
  210. freedSpace += entry.size;
  211. logMessage(`Evicted cached preview for ${entry.pattern} (${(entry.size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
  212. resolve();
  213. };
  214. deleteRequest.onerror = () => reject(deleteRequest.error);
  215. });
  216. }
  217. logMessage(`Freed ${(freedSpace / 1024 / 1024).toFixed(1)}MB from preview cache`, LOG_TYPE.DEBUG);
  218. } catch (error) {
  219. logMessage(`Error managing cache size: ${error.message}`, LOG_TYPE.WARNING);
  220. }
  221. }
  222. // Get current cache size
  223. async function getPreviewCacheSize() {
  224. try {
  225. if (!previewCacheDB) return 0;
  226. const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly');
  227. const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
  228. return new Promise((resolve, reject) => {
  229. const request = store.getAll();
  230. request.onsuccess = () => {
  231. const totalSize = request.result.reduce((sum, entry) => sum + (entry.size || 0), 0);
  232. resolve(totalSize);
  233. };
  234. request.onerror = () => reject(request.error);
  235. });
  236. } catch (error) {
  237. logMessage(`Error getting cache size: ${error.message}`, LOG_TYPE.WARNING);
  238. return 0;
  239. }
  240. }
  241. // Preload images in batch
  242. async function preloadImages(urls) {
  243. const promises = urls.map(url => {
  244. return new Promise((resolve, reject) => {
  245. if (imageCache.has(url)) {
  246. resolve(imageCache.get(url));
  247. return;
  248. }
  249. const img = new Image();
  250. img.onload = () => {
  251. imageCache.set(url, img);
  252. resolve(img);
  253. };
  254. img.onerror = reject;
  255. img.src = url;
  256. });
  257. });
  258. return Promise.allSettled(promises);
  259. }
  260. // Initialize Intersection Observer for lazy loading
  261. function initPreviewObserver() {
  262. if (previewObserver) {
  263. previewObserver.disconnect();
  264. }
  265. previewObserver = new IntersectionObserver((entries) => {
  266. entries.forEach(entry => {
  267. if (entry.isIntersecting) {
  268. const previewContainer = entry.target;
  269. const pattern = previewContainer.dataset.pattern;
  270. if (pattern) {
  271. addPatternToBatch(pattern, previewContainer);
  272. previewObserver.unobserve(previewContainer);
  273. }
  274. }
  275. });
  276. }, {
  277. rootMargin: '200px 0px', // Reduced margin for more precise loading
  278. threshold: 0.1
  279. });
  280. }
  281. // Add pattern to pending batch for efficient loading
  282. async function addPatternToBatch(pattern, element) {
  283. // Check in-memory cache first
  284. if (previewCache.has(pattern)) {
  285. const previewData = previewCache.get(pattern);
  286. if (previewData && !previewData.error) {
  287. if (element) {
  288. updatePreviewElement(element, previewData.image_data);
  289. }
  290. }
  291. return;
  292. }
  293. // Check IndexedDB cache
  294. const cachedData = await getPreviewFromCache(pattern);
  295. if (cachedData && !cachedData.error) {
  296. // Add to in-memory cache for faster access
  297. previewCache.set(pattern, cachedData);
  298. if (element) {
  299. updatePreviewElement(element, cachedData.image_data);
  300. }
  301. return;
  302. }
  303. // Check if this is a newly uploaded pattern
  304. const isNewUpload = element?.dataset.isNewUpload === 'true';
  305. // Reset retry flags when starting fresh
  306. if (element) {
  307. element.dataset.retryCount = '0';
  308. element.dataset.hasTriedIndividual = 'false';
  309. }
  310. // Add loading indicator with better styling
  311. if (!element.querySelector('img')) {
  312. const loadingText = isNewUpload ? 'Generating preview...' : 'Loading...';
  313. element.innerHTML = `
  314. <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
  315. <div class="bg-slate-200 rounded-full h-8 w-8 flex items-center justify-center">
  316. <div class="bg-slate-500 rounded-full h-4 w-4"></div>
  317. </div>
  318. </div>
  319. <div class="absolute inset-0 flex items-center justify-center">
  320. <div class="text-xs text-slate-500 mt-12">${loadingText}</div>
  321. </div>
  322. `;
  323. }
  324. // Add to pending batch
  325. pendingPatterns.set(pattern, element);
  326. // Process batch immediately if it's full or if it's a new upload
  327. if (pendingPatterns.size >= LAZY_BATCH_SIZE || isNewUpload) {
  328. processPendingBatch();
  329. }
  330. }
  331. // Update preview element with smooth transition
  332. function updatePreviewElement(element, imageUrl) {
  333. const img = new Image();
  334. img.onload = () => {
  335. element.innerHTML = '';
  336. element.appendChild(img);
  337. img.className = 'w-full h-full object-contain transition-opacity duration-300';
  338. img.style.opacity = '0';
  339. requestAnimationFrame(() => {
  340. img.style.opacity = '1';
  341. });
  342. };
  343. img.src = imageUrl;
  344. img.alt = 'Pattern Preview';
  345. }
  346. // Process pending patterns in batches
  347. async function processPendingBatch() {
  348. if (pendingPatterns.size === 0) return;
  349. // Create a copy of current pending patterns and clear the original
  350. const currentBatch = new Map(pendingPatterns);
  351. pendingPatterns.clear();
  352. const patternsToLoad = Array.from(currentBatch.keys());
  353. try {
  354. logMessage(`Loading batch of ${patternsToLoad.length} pattern previews`, LOG_TYPE.DEBUG);
  355. const response = await fetch('/preview_thr_batch', {
  356. method: 'POST',
  357. headers: { 'Content-Type': 'application/json' },
  358. body: JSON.stringify({ file_names: patternsToLoad })
  359. });
  360. if (response.ok) {
  361. const results = await response.json();
  362. // Process all results
  363. for (const [pattern, data] of Object.entries(results)) {
  364. const element = currentBatch.get(pattern);
  365. if (data && !data.error && data.image_data) {
  366. // Cache in memory with size limit
  367. if (previewCache.size > 100) { // Limit cache size
  368. const oldestKey = previewCache.keys().next().value;
  369. previewCache.delete(oldestKey);
  370. }
  371. previewCache.set(pattern, data);
  372. // Save to IndexedDB cache for persistence
  373. await savePreviewToCache(pattern, data);
  374. if (element) {
  375. updatePreviewElement(element, data.image_data);
  376. }
  377. } else {
  378. handleLoadError(pattern, element, data?.error || 'Failed to load preview');
  379. }
  380. }
  381. }
  382. } catch (error) {
  383. logMessage(`Error loading preview batch: ${error.message}`, LOG_TYPE.ERROR);
  384. // Handle error for each pattern in batch
  385. for (const pattern of patternsToLoad) {
  386. const element = currentBatch.get(pattern);
  387. handleLoadError(pattern, element, error.message);
  388. }
  389. }
  390. }
  391. // Trigger preview loading for currently visible patterns
  392. function triggerPreviewLoadingForVisible() {
  393. // Get all pattern cards currently in the DOM
  394. const patternCards = document.querySelectorAll('.pattern-card');
  395. patternCards.forEach(card => {
  396. const pattern = card.dataset.pattern;
  397. const previewContainer = card.querySelector('.pattern-preview');
  398. // Check if this pattern needs preview loading
  399. if (pattern && !previewCache.has(pattern) && !pendingPatterns.has(pattern)) {
  400. // Add to batch for immediate loading
  401. addPatternToBatch(pattern, previewContainer);
  402. }
  403. });
  404. // Process any pending previews immediately
  405. if (pendingPatterns.size > 0) {
  406. processPendingBatch();
  407. }
  408. }
  409. // Load individual pattern preview (fallback when batch loading fails)
  410. async function loadIndividualPreview(pattern, element) {
  411. try {
  412. logMessage(`Loading individual preview for ${pattern}`, LOG_TYPE.DEBUG);
  413. const response = await fetch('/preview_thr_batch', {
  414. method: 'POST',
  415. headers: { 'Content-Type': 'application/json' },
  416. body: JSON.stringify({ file_names: [pattern] })
  417. });
  418. if (response.ok) {
  419. const results = await response.json();
  420. const data = results[pattern];
  421. if (data && !data.error && data.image_data) {
  422. // Cache in memory with size limit
  423. if (previewCache.size > 100) { // Limit cache size
  424. const oldestKey = previewCache.keys().next().value;
  425. previewCache.delete(oldestKey);
  426. }
  427. previewCache.set(pattern, data);
  428. // Save to IndexedDB cache for persistence
  429. await savePreviewToCache(pattern, data);
  430. if (element) {
  431. updatePreviewElement(element, data.image_data);
  432. }
  433. logMessage(`Individual preview loaded successfully for ${pattern}`, LOG_TYPE.DEBUG);
  434. } else {
  435. throw new Error(data?.error || 'Failed to load preview data');
  436. }
  437. } else {
  438. throw new Error(`HTTP error! status: ${response.status}`);
  439. }
  440. } catch (error) {
  441. logMessage(`Error loading individual preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
  442. // Continue with normal error handling
  443. handleLoadError(pattern, element, error.message);
  444. }
  445. }
  446. // Handle load errors with retry logic
  447. function handleLoadError(pattern, element, error) {
  448. const retryCount = element.dataset.retryCount || 0;
  449. const isNewUpload = element.dataset.isNewUpload === 'true';
  450. const hasTriedIndividual = element.dataset.hasTriedIndividual === 'true';
  451. // Use longer delays for newly uploaded patterns
  452. const retryDelay = isNewUpload ? RETRY_DELAY * 2 : RETRY_DELAY;
  453. const maxRetries = isNewUpload ? MAX_RETRIES * 2 : MAX_RETRIES;
  454. if (retryCount < maxRetries) {
  455. // Update retry count
  456. element.dataset.retryCount = parseInt(retryCount) + 1;
  457. // Determine retry strategy
  458. let retryStrategy = 'batch';
  459. if (retryCount >= 1 && !hasTriedIndividual) {
  460. // After first batch attempt fails, try individual loading
  461. retryStrategy = 'individual';
  462. element.dataset.hasTriedIndividual = 'true';
  463. }
  464. // Show retry message with different text for new uploads and retry strategies
  465. let retryText;
  466. if (isNewUpload) {
  467. retryText = retryStrategy === 'individual' ?
  468. `Trying individual load... (${retryCount + 1}/${maxRetries})` :
  469. `Generating preview... (${retryCount + 1}/${maxRetries})`;
  470. } else {
  471. retryText = retryStrategy === 'individual' ?
  472. `Trying individual load... (${retryCount + 1}/${maxRetries})` :
  473. `Retrying... (${retryCount + 1}/${maxRetries})`;
  474. }
  475. element.innerHTML = `
  476. <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
  477. <div class="text-xs text-slate-500 text-center">
  478. <div>${isNewUpload ? 'Processing new pattern' : 'Failed to load'}</div>
  479. <div>${retryText}</div>
  480. </div>
  481. </div>
  482. `;
  483. // Retry after delay with appropriate strategy
  484. setTimeout(() => {
  485. if (retryStrategy === 'individual') {
  486. loadIndividualPreview(pattern, element);
  487. } else {
  488. addPatternToBatch(pattern, element);
  489. }
  490. }, retryDelay);
  491. } else {
  492. // Show final error state
  493. element.innerHTML = `
  494. <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
  495. <div class="text-xs text-slate-500 text-center">
  496. <div>Failed to load</div>
  497. <div>Click to retry</div>
  498. </div>
  499. </div>
  500. `;
  501. // Add click handler for manual retry
  502. element.onclick = () => {
  503. element.dataset.retryCount = '0';
  504. element.dataset.hasTriedIndividual = 'false';
  505. addPatternToBatch(pattern, element);
  506. };
  507. }
  508. previewCache.set(pattern, { error: true });
  509. }
  510. // Load and display patterns
  511. async function loadPatterns(forceRefresh = false) {
  512. try {
  513. logMessage('Loading patterns...', LOG_TYPE.INFO);
  514. logMessage('Fetching fresh patterns list from server', LOG_TYPE.DEBUG);
  515. const response = await fetch('/list_theta_rho_files');
  516. const allFiles = await response.json();
  517. logMessage(`Received ${allFiles.length} files from server`, LOG_TYPE.INFO);
  518. // Filter for .thr files
  519. let patterns = allFiles.filter(file => file.endsWith('.thr'));
  520. logMessage(`Filtered to ${patterns.length} .thr files`, LOG_TYPE.INFO);
  521. if (forceRefresh) {
  522. showStatusMessage('Patterns list refreshed successfully', 'success');
  523. }
  524. // Sort patterns with custom_patterns on top and all alphabetically sorted
  525. const sortedPatterns = patterns.sort((a, b) => {
  526. const isCustomA = a.startsWith('custom_patterns/');
  527. const isCustomB = b.startsWith('custom_patterns/');
  528. if (isCustomA && !isCustomB) return -1;
  529. if (!isCustomA && isCustomB) return 1;
  530. return a.localeCompare(b);
  531. });
  532. allPatterns = sortedPatterns;
  533. currentBatch = 0;
  534. logMessage('Displaying initial batch of patterns...', LOG_TYPE.INFO);
  535. displayPatternBatch();
  536. logMessage('Initial batch loaded successfully.', LOG_TYPE.SUCCESS);
  537. } catch (error) {
  538. logMessage(`Error loading patterns: ${error.message}`, LOG_TYPE.ERROR);
  539. console.error('Full error:', error);
  540. showStatusMessage('Failed to load patterns', 'error');
  541. }
  542. }
  543. // Display a batch of patterns with improved initial load
  544. function displayPatternBatch() {
  545. const patternGrid = document.querySelector('.grid');
  546. if (!patternGrid) {
  547. logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
  548. return;
  549. }
  550. const start = currentBatch * BATCH_SIZE;
  551. const end = Math.min(start + BATCH_SIZE, allPatterns.length);
  552. const batchPatterns = allPatterns.slice(start, end);
  553. // Display batch patterns
  554. batchPatterns.forEach(pattern => {
  555. const patternCard = createPatternCard(pattern);
  556. patternGrid.appendChild(patternCard);
  557. });
  558. // If there are more patterns to load, set up the observer for the last few cards
  559. if (end < allPatterns.length) {
  560. const lastCards = Array.from(patternGrid.children).slice(-3); // Observe last 3 cards
  561. lastCards.forEach(card => {
  562. const observer = new IntersectionObserver((entries) => {
  563. if (entries[0].isIntersecting) {
  564. currentBatch++;
  565. displayPatternBatch();
  566. observer.disconnect();
  567. }
  568. }, {
  569. rootMargin: '200px 0px',
  570. threshold: 0.1
  571. });
  572. observer.observe(card);
  573. });
  574. }
  575. }
  576. // Create a pattern card element
  577. function createPatternCard(pattern) {
  578. const card = document.createElement('div');
  579. card.className = 'pattern-card flex flex-col items-center gap-3 bg-gray-50';
  580. card.dataset.pattern = pattern;
  581. // Create preview container with proper styling for loading indicator
  582. const previewContainer = document.createElement('div');
  583. previewContainer.className = 'w-32 h-32 rounded-full shadow-md relative pattern-preview group';
  584. previewContainer.dataset.pattern = pattern;
  585. // Add loading indicator
  586. previewContainer.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="bg-slate-200 rounded-full h-8 w-8 flex items-center justify-center"><div class="bg-slate-500 rounded-full h-4 w-4"></div></div></div>';
  587. // Add play button overlay (hidden by default, shown on hover)
  588. const playOverlay = document.createElement('div');
  589. playOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer';
  590. playOverlay.innerHTML = '<div class="bg-white rounded-full p-2 shadow-lg flex items-center justify-center w-10 h-10"><span class="material-icons text-lg text-gray-800">play_arrow</span></div>';
  591. // Add click handler for play button (separate from card click)
  592. playOverlay.addEventListener('click', (e) => {
  593. e.stopPropagation(); // Prevent card selection
  594. openAnimatedPreview(pattern);
  595. });
  596. previewContainer.appendChild(playOverlay);
  597. // Create pattern name
  598. const patternName = document.createElement('p');
  599. patternName.className = 'text-gray-700 text-sm font-medium text-center truncate w-full';
  600. patternName.textContent = pattern.replace('.thr', '').split('/').pop();
  601. // Add click handler
  602. card.onclick = () => selectPattern(pattern, card);
  603. // Check if preview is already in cache
  604. const previewData = previewCache.get(pattern);
  605. if (previewData && !previewData.error && previewData.image_data) {
  606. updatePreviewElement(previewContainer, previewData.image_data);
  607. } else {
  608. // Start observing the preview container for lazy loading
  609. previewObserver.observe(previewContainer);
  610. }
  611. card.appendChild(previewContainer);
  612. card.appendChild(patternName);
  613. return card;
  614. }
  615. // Select a pattern
  616. function selectPattern(pattern, card) {
  617. // Remove selected class from all cards
  618. document.querySelectorAll('.pattern-card').forEach(c => {
  619. c.classList.remove('selected');
  620. });
  621. // Add selected class to clicked card
  622. card.classList.add('selected');
  623. // Show pattern preview
  624. showPatternPreview(pattern);
  625. }
  626. // Show pattern preview
  627. async function showPatternPreview(pattern) {
  628. try {
  629. // Check in-memory cache first
  630. let data = previewCache.get(pattern);
  631. // If not in cache, fetch it
  632. if (!data) {
  633. const response = await fetch('/preview_thr_batch', {
  634. method: 'POST',
  635. headers: { 'Content-Type': 'application/json' },
  636. body: JSON.stringify({ file_names: [pattern] })
  637. });
  638. if (!response.ok) {
  639. throw new Error(`HTTP error! status: ${response.status}`);
  640. }
  641. const results = await response.json();
  642. data = results[pattern];
  643. if (data && !data.error) {
  644. // Cache in memory
  645. previewCache.set(pattern, data);
  646. } else {
  647. throw new Error(data?.error || 'Failed to get preview data');
  648. }
  649. }
  650. const previewPanel = document.getElementById('patternPreviewPanel');
  651. const layoutContainer = document.querySelector('.layout-content-container');
  652. // Update preview content
  653. if (data.image_data) {
  654. document.getElementById('patternPreviewImage').src = data.image_data;
  655. }
  656. // Set pattern name in the preview panel
  657. const patternName = pattern.replace('.thr', '').split('/').pop();
  658. document.getElementById('patternPreviewTitle').textContent = patternName;
  659. // Format and display coordinates
  660. const formatCoordinate = (coord) => {
  661. if (!coord) return '(0, 0)';
  662. const x = coord.x !== undefined ? coord.x.toFixed(1) : '0.0';
  663. const y = coord.y !== undefined ? coord.y.toFixed(1) : '0.0';
  664. return `(${x}, ${y})`;
  665. };
  666. document.getElementById('firstCoordinate').textContent = formatCoordinate(data.first_coordinate);
  667. document.getElementById('lastCoordinate').textContent = formatCoordinate(data.last_coordinate);
  668. // Show preview panel
  669. previewPanel.classList.remove('translate-x-full');
  670. if (window.innerWidth >= 1024) {
  671. // For large screens, show preview alongside content
  672. layoutContainer.parentElement.classList.add('preview-open');
  673. previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
  674. } else {
  675. // For small screens, show preview as overlay
  676. layoutContainer.parentElement.classList.remove('preview-open');
  677. }
  678. // Setup preview panel events
  679. setupPreviewPanelEvents(pattern);
  680. } catch (error) {
  681. logMessage(`Error showing preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
  682. // Show error state in preview panel instead of hiding it
  683. showPreviewError(pattern, error.message);
  684. }
  685. }
  686. function showPreviewError(pattern, errorMessage) {
  687. const previewPanel = document.getElementById('patternPreviewPanel');
  688. const layoutContainer = document.querySelector('.layout-content-container');
  689. // Show error state in preview panel
  690. const patternName = pattern.replace('.thr', '').split('/').pop();
  691. document.getElementById('patternPreviewTitle').textContent = `Error: ${patternName}`;
  692. // Show error image or placeholder
  693. const img = document.getElementById('patternPreviewImage');
  694. img.src = 'data:image/svg+xml;base64,' + btoa(`
  695. <svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
  696. <rect width="100%" height="100%" fill="#f3f4f6"/>
  697. <text x="50%" y="40%" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6b7280">
  698. Pattern Not Found
  699. </text>
  700. <text x="50%" y="60%" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#9ca3af">
  701. ${patternName}
  702. </text>
  703. <text x="50%" y="75%" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#ef4444">
  704. File may have been deleted
  705. </text>
  706. </svg>
  707. `);
  708. // Clear coordinates
  709. document.getElementById('firstCoordinate').textContent = '(0, 0)';
  710. document.getElementById('lastCoordinate').textContent = '(0, 0)';
  711. // Show preview panel with error state
  712. previewPanel.classList.remove('translate-x-full');
  713. if (window.innerWidth >= 1024) {
  714. layoutContainer.parentElement.classList.add('preview-open');
  715. previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
  716. } else {
  717. layoutContainer.parentElement.classList.remove('preview-open');
  718. }
  719. // Setup events so user can still close the panel
  720. setupPreviewPanelEvents(pattern);
  721. }
  722. function hidePatternPreview() {
  723. const previewPanel = document.getElementById('patternPreviewPanel');
  724. const layoutContainer = document.querySelector('.layout-content-container');
  725. previewPanel.classList.add('translate-x-full');
  726. if (window.innerWidth >= 1024) {
  727. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  728. }
  729. layoutContainer.parentElement.classList.remove('preview-open');
  730. }
  731. // Add window resize handler
  732. window.addEventListener('resize', () => {
  733. const previewPanel = document.getElementById('patternPreviewPanel');
  734. const layoutContainer = document.querySelector('.layout-content-container');
  735. if (window.innerWidth >= 1024) {
  736. if (!previewPanel.classList.contains('translate-x-full')) {
  737. layoutContainer.parentElement.classList.add('preview-open');
  738. previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
  739. }
  740. } else {
  741. layoutContainer.parentElement.classList.remove('preview-open');
  742. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  743. }
  744. });
  745. // Setup preview panel events
  746. function setupPreviewPanelEvents(pattern) {
  747. const panel = document.getElementById('patternPreviewPanel');
  748. const closeButton = document.getElementById('closePreviewPanel');
  749. const playButton = document.getElementById('playPattern');
  750. const deleteButton = document.getElementById('deletePattern');
  751. const preExecutionInputs = document.querySelectorAll('input[name="preExecutionAction"]');
  752. const previewPlayOverlay = document.getElementById('previewPlayOverlay');
  753. // Close panel when clicking the close button
  754. closeButton.onclick = () => {
  755. hidePatternPreview();
  756. // Remove selected state from all cards when closing
  757. document.querySelectorAll('.pattern-card').forEach(c => {
  758. c.classList.remove('selected');
  759. });
  760. };
  761. // Handle play button overlay click in preview panel
  762. if (previewPlayOverlay) {
  763. previewPlayOverlay.onclick = () => {
  764. openAnimatedPreview(pattern);
  765. };
  766. }
  767. // Handle play button click
  768. playButton.onclick = async () => {
  769. if (!pattern) {
  770. showStatusMessage('No pattern selected', 'error');
  771. return;
  772. }
  773. try {
  774. // Show the preview modal
  775. if (window.openPlayerPreviewModal) {
  776. window.openPlayerPreviewModal();
  777. }
  778. // Get the selected pre-execution action
  779. const preExecutionInput = document.querySelector('input[name="preExecutionAction"]:checked');
  780. const preExecution = preExecutionInput ? preExecutionInput.value : 'none';
  781. const response = await fetch('/run_theta_rho', {
  782. method: 'POST',
  783. headers: {
  784. 'Content-Type': 'application/json'
  785. },
  786. body: JSON.stringify({
  787. file_name: pattern,
  788. pre_execution: preExecution
  789. })
  790. });
  791. const data = await response.json();
  792. if (response.ok) {
  793. showStatusMessage(`Running pattern: ${pattern.split('/').pop()}`, 'success');
  794. hidePatternPreview();
  795. } else {
  796. let errorMsg = data.detail || 'Failed to run pattern';
  797. let errorType = 'error';
  798. // Handle specific error cases with appropriate messaging
  799. if (data.detail === 'Connection not established') {
  800. errorMsg = 'Please connect to the device before running a pattern';
  801. errorType = 'warning';
  802. } else if (response.status === 409) {
  803. errorMsg = 'Another pattern is already running. Please stop the current pattern first.';
  804. errorType = 'warning';
  805. } else if (response.status === 404) {
  806. errorMsg = 'Pattern file not found. Please refresh the page and try again.';
  807. errorType = 'error';
  808. } else if (response.status === 400) {
  809. errorMsg = 'Invalid request. Please check your settings and try again.';
  810. errorType = 'error';
  811. } else if (response.status === 500) {
  812. errorMsg = 'Server error. Please try again later.';
  813. errorType = 'error';
  814. }
  815. showStatusMessage(errorMsg, errorType);
  816. return;
  817. }
  818. } catch (error) {
  819. console.error('Error running pattern:', error);
  820. // Handle network errors specifically
  821. if (error.name === 'TypeError' && error.message.includes('fetch')) {
  822. showStatusMessage('Network error. Please check your connection and try again.', 'error');
  823. } else if (error.message && error.message.includes('409')) {
  824. showStatusMessage('Another pattern is already running', 'warning');
  825. } else if (error.message) {
  826. showStatusMessage(error.message, 'error');
  827. } else {
  828. showStatusMessage('Failed to run pattern', 'error');
  829. }
  830. }
  831. };
  832. // Handle delete button click
  833. deleteButton.onclick = async () => {
  834. if (!pattern.startsWith('custom_patterns/')) {
  835. logMessage('Cannot delete built-in patterns', LOG_TYPE.WARNING);
  836. showStatusMessage('Cannot delete built-in patterns', 'warning');
  837. return;
  838. }
  839. if (confirm('Are you sure you want to delete this pattern?')) {
  840. try {
  841. logMessage(`Deleting pattern: ${pattern}`, LOG_TYPE.INFO);
  842. const response = await fetch('/delete_theta_rho_file', {
  843. method: 'POST',
  844. headers: {
  845. 'Content-Type': 'application/json'
  846. },
  847. body: JSON.stringify({ file_name: pattern })
  848. });
  849. if (!response.ok) {
  850. throw new Error(`HTTP error! status: ${response.status}`);
  851. }
  852. const result = await response.json();
  853. if (result.success) {
  854. logMessage(`Pattern deleted successfully: ${pattern}`, LOG_TYPE.SUCCESS);
  855. showStatusMessage(`Pattern "${pattern.split('/').pop()}" deleted successfully`);
  856. // Clear from in-memory caches
  857. previewCache.delete(pattern);
  858. imageCache.delete(pattern);
  859. // Clear from IndexedDB cache
  860. await clearPatternFromIndexedDB(pattern);
  861. // Clear from localStorage patterns list cache
  862. const cachedPatterns = JSON.parse(localStorage.getItem(PATTERNS_CACHE_KEY) || '{}');
  863. if (cachedPatterns.data) {
  864. const index = cachedPatterns.data.indexOf(pattern);
  865. if (index > -1) {
  866. cachedPatterns.data.splice(index, 1);
  867. localStorage.setItem(PATTERNS_CACHE_KEY, JSON.stringify(cachedPatterns));
  868. }
  869. }
  870. // Remove the pattern card
  871. const selectedCard = document.querySelector('.pattern-card.selected');
  872. if (selectedCard) {
  873. selectedCard.remove();
  874. }
  875. // Close the preview panel
  876. const previewPanel = document.getElementById('patternPreviewPanel');
  877. const layoutContainer = document.querySelector('.layout-content-container');
  878. previewPanel.classList.add('translate-x-full');
  879. if (window.innerWidth >= 1024) {
  880. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  881. }
  882. layoutContainer.parentElement.classList.remove('preview-open');
  883. // Clear the preview panel content
  884. document.getElementById('patternPreviewImage').src = '';
  885. document.getElementById('patternPreviewTitle').textContent = 'Pattern Details';
  886. document.getElementById('firstCoordinate').textContent = '(0, 0)';
  887. document.getElementById('lastCoordinate').textContent = '(0, 0)';
  888. // Refresh the pattern list (force refresh since pattern was deleted)
  889. await loadPatterns(true);
  890. } else {
  891. throw new Error(result.error || 'Unknown error');
  892. }
  893. } catch (error) {
  894. logMessage(`Failed to delete pattern: ${error.message}`, LOG_TYPE.ERROR);
  895. showStatusMessage(`Failed to delete pattern: ${error.message}`, 'error');
  896. }
  897. }
  898. };
  899. // Handle pre-execution action changes
  900. preExecutionInputs.forEach(input => {
  901. input.onchange = () => {
  902. const action = input.parentElement.textContent.trim();
  903. logMessage(`Pre-execution action changed to: ${action}`, LOG_TYPE.INFO);
  904. };
  905. });
  906. }
  907. // Search patterns
  908. function searchPatterns(query) {
  909. if (!query) {
  910. // If search is empty, clear grid and show all patterns
  911. const patternGrid = document.querySelector('.grid');
  912. if (patternGrid) {
  913. patternGrid.innerHTML = '';
  914. }
  915. // Reset current batch and display from beginning
  916. currentBatch = 0;
  917. displayPatternBatch();
  918. return;
  919. }
  920. const searchInput = query.toLowerCase();
  921. const patternGrid = document.querySelector('.grid');
  922. if (!patternGrid) {
  923. logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
  924. return;
  925. }
  926. // Clear existing patterns
  927. patternGrid.innerHTML = '';
  928. // Filter patterns
  929. const filteredPatterns = allPatterns.filter(pattern =>
  930. pattern.toLowerCase().includes(searchInput)
  931. );
  932. // Display filtered patterns
  933. filteredPatterns.forEach(pattern => {
  934. const patternCard = createPatternCard(pattern);
  935. patternGrid.appendChild(patternCard);
  936. });
  937. // Give the browser a chance to render the cards
  938. requestAnimationFrame(() => {
  939. // Trigger preview loading for the search results
  940. triggerPreviewLoadingForVisible();
  941. });
  942. logMessage(`Showing ${filteredPatterns.length} patterns matching "${query}"`, LOG_TYPE.INFO);
  943. }
  944. // Filter patterns by category
  945. function filterPatternsByCategory(category) {
  946. // TODO: Implement category filtering logic
  947. logMessage(`Filtering patterns by category: ${category}`, LOG_TYPE.INFO);
  948. }
  949. // Filter patterns by tag
  950. function filterPatternsByTag(tag) {
  951. // TODO: Implement tag filtering logic
  952. logMessage(`Filtering patterns by tag: ${tag}`, LOG_TYPE.INFO);
  953. }
  954. // Initialize the patterns page
  955. document.addEventListener('DOMContentLoaded', async () => {
  956. try {
  957. logMessage('Initializing patterns page...', LOG_TYPE.DEBUG);
  958. // Initialize IndexedDB preview cache (shared with playlists page)
  959. await initPreviewCacheDB();
  960. // Setup upload event handlers
  961. setupUploadEventHandlers();
  962. // Initialize intersection observer for lazy loading
  963. initPreviewObserver();
  964. // Setup search functionality
  965. const searchInput = document.getElementById('patternSearch');
  966. const searchButton = document.getElementById('searchButton');
  967. const cacheAllButton = document.getElementById('cacheAllButton');
  968. if (searchInput && searchButton) {
  969. // Search on button click
  970. searchButton.addEventListener('click', () => {
  971. searchPatterns(searchInput.value.trim());
  972. });
  973. // Search on Enter key
  974. searchInput.addEventListener('keypress', (e) => {
  975. if (e.key === 'Enter') {
  976. searchPatterns(searchInput.value.trim());
  977. }
  978. });
  979. // Clear search when input is empty
  980. searchInput.addEventListener('input', (e) => {
  981. if (e.target.value.trim() === '') {
  982. searchPatterns('');
  983. }
  984. });
  985. }
  986. // Setup cache all button - now triggers the modal
  987. if (cacheAllButton) {
  988. cacheAllButton.addEventListener('click', () => {
  989. // Always show the modal when manually clicked, using forceShow parameter
  990. if (typeof showCacheAllPrompt === 'function') {
  991. showCacheAllPrompt(true); // true = forceShow
  992. } else {
  993. // Fallback if function not available
  994. const modal = document.getElementById('cacheAllPromptModal');
  995. if (modal) {
  996. modal.classList.remove('hidden');
  997. modal.dataset.manuallyTriggered = 'true';
  998. }
  999. }
  1000. });
  1001. }
  1002. // Load patterns on page load
  1003. await loadPatterns();
  1004. logMessage('Patterns page initialized successfully', LOG_TYPE.SUCCESS);
  1005. } catch (error) {
  1006. logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
  1007. }
  1008. });
  1009. function updateCurrentlyPlayingUI(status) {
  1010. // Get all required DOM elements once
  1011. const container = document.getElementById('currently-playing-container');
  1012. const fileNameElement = document.getElementById('currently-playing-file');
  1013. const progressBar = document.getElementById('play_progress');
  1014. const progressText = document.getElementById('play_progress_text');
  1015. const pausePlayButton = document.getElementById('pausePlayCurrent');
  1016. const speedDisplay = document.getElementById('current_speed_display');
  1017. const speedInput = document.getElementById('speedInput');
  1018. // Check if all required elements exist
  1019. if (!container || !fileNameElement || !progressBar || !progressText) {
  1020. console.log('Required DOM elements not found:', {
  1021. container: !!container,
  1022. fileNameElement: !!fileNameElement,
  1023. progressBar: !!progressBar,
  1024. progressText: !!progressText
  1025. });
  1026. setTimeout(() => updateCurrentlyPlayingUI(status), 100);
  1027. return;
  1028. }
  1029. // Update container visibility based on status
  1030. if (status.current_file && status.is_running) {
  1031. document.body.classList.add('playing');
  1032. container.style.display = 'flex';
  1033. } else {
  1034. document.body.classList.remove('playing');
  1035. container.style.display = 'none';
  1036. }
  1037. // Update file name display
  1038. if (status.current_file) {
  1039. const fileName = normalizeFilePath(status.current_file);
  1040. fileNameElement.textContent = fileName;
  1041. } else {
  1042. fileNameElement.textContent = 'No pattern playing';
  1043. }
  1044. // Update next file display
  1045. const nextFileElement = document.getElementById('next-file');
  1046. if (nextFileElement) {
  1047. if (status.playlist && status.playlist.next_file) {
  1048. const nextFileName = normalizeFilePath(status.playlist.next_file);
  1049. nextFileElement.textContent = `(Next: ${nextFileName})`;
  1050. nextFileElement.style.display = 'block';
  1051. } else {
  1052. nextFileElement.style.display = 'none';
  1053. }
  1054. }
  1055. // Update speed display and input if they exist
  1056. if (status.speed) {
  1057. if (speedDisplay) {
  1058. speedDisplay.textContent = `Current Speed: ${status.speed}`;
  1059. }
  1060. if (speedInput) {
  1061. speedInput.value = status.speed;
  1062. }
  1063. }
  1064. // Update pattern preview if it's a new pattern
  1065. // ... existing code ...
  1066. }
  1067. // Setup upload event handlers
  1068. function setupUploadEventHandlers() {
  1069. // Upload file input handler
  1070. document.getElementById('patternFileInput').addEventListener('change', async function(e) {
  1071. const file = e.target.files[0];
  1072. if (!file) return;
  1073. try {
  1074. const formData = new FormData();
  1075. formData.append('file', file);
  1076. const response = await fetch('/upload_theta_rho', {
  1077. method: 'POST',
  1078. body: formData
  1079. });
  1080. const result = await response.json();
  1081. if (result.success) {
  1082. showStatusMessage(`Pattern "${file.name}" uploaded successfully`);
  1083. // Clear any existing cache for this pattern to ensure fresh loading
  1084. const newPatternPath = `custom_patterns/${file.name}`;
  1085. previewCache.delete(newPatternPath);
  1086. // Add a small delay to allow backend preview generation to complete
  1087. await new Promise(resolve => setTimeout(resolve, 1000));
  1088. // Refresh the pattern list (force refresh since new pattern was uploaded)
  1089. await loadPatterns(true);
  1090. // Clear the file input
  1091. e.target.value = '';
  1092. // Trigger preview loading for newly uploaded patterns with extended retry
  1093. setTimeout(() => {
  1094. const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
  1095. if (newPatternCard) {
  1096. const previewContainer = newPatternCard.querySelector('.pattern-preview');
  1097. if (previewContainer) {
  1098. // Clear any existing retry count and force reload
  1099. previewContainer.dataset.retryCount = '0';
  1100. previewContainer.dataset.hasTriedIndividual = 'false';
  1101. previewContainer.dataset.isNewUpload = 'true';
  1102. addPatternToBatch(newPatternPath, previewContainer);
  1103. }
  1104. }
  1105. }, 500);
  1106. } else {
  1107. showStatusMessage(`Failed to upload pattern: ${result.error}`, 'error');
  1108. }
  1109. } catch (error) {
  1110. console.error('Error uploading pattern:', error);
  1111. showStatusMessage(`Error uploading pattern: ${error.message}`, 'error');
  1112. }
  1113. });
  1114. // Pattern deletion handler
  1115. const deleteModal = document.getElementById('deleteConfirmModal');
  1116. if (deleteModal) {
  1117. const confirmBtn = deleteModal.querySelector('#confirmDeleteBtn');
  1118. const cancelBtn = deleteModal.querySelector('#cancelDeleteBtn');
  1119. if (confirmBtn) {
  1120. confirmBtn.addEventListener('click', async () => {
  1121. const patternToDelete = confirmBtn.dataset.pattern;
  1122. if (patternToDelete) {
  1123. await deletePattern(patternToDelete);
  1124. // Force refresh after deletion
  1125. await loadPatterns(true);
  1126. }
  1127. deleteModal.classList.add('hidden');
  1128. });
  1129. }
  1130. if (cancelBtn) {
  1131. cancelBtn.addEventListener('click', () => {
  1132. deleteModal.classList.add('hidden');
  1133. });
  1134. }
  1135. }
  1136. }
  1137. // Cache all pattern previews
  1138. async function cacheAllPreviews() {
  1139. const cacheAllButton = document.getElementById('cacheAllButton');
  1140. if (!cacheAllButton) return;
  1141. try {
  1142. // Disable button and show loading state
  1143. cacheAllButton.disabled = true;
  1144. // Get current cache size
  1145. const currentSize = await getPreviewCacheSize();
  1146. const maxSize = MAX_CACHE_SIZE_BYTES || (200 * 1024 * 1024); // 200MB default
  1147. if (currentSize > maxSize) {
  1148. // Clear cache if it's too large
  1149. await clearPreviewCache();
  1150. // Also clear progress since we're starting fresh
  1151. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1152. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1153. }
  1154. // Get all patterns that aren't cached yet
  1155. const uncachedPatterns = allPatterns.filter(pattern => !previewCache.has(pattern));
  1156. if (uncachedPatterns.length === 0) {
  1157. showStatusMessage('All patterns are already cached!', 'info');
  1158. return;
  1159. }
  1160. // Check for existing progress
  1161. let startIndex = 0;
  1162. const savedProgress = localStorage.getItem(CACHE_PROGRESS_KEY);
  1163. const savedTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
  1164. if (savedProgress && savedTimestamp) {
  1165. const progressAge = Date.now() - parseInt(savedTimestamp);
  1166. if (progressAge < CACHE_PROGRESS_EXPIRY) {
  1167. const lastCachedPattern = savedProgress;
  1168. const lastIndex = uncachedPatterns.findIndex(p => p === lastCachedPattern);
  1169. if (lastIndex !== -1) {
  1170. startIndex = lastIndex + 1;
  1171. showStatusMessage('Resuming from previous progress...', 'info');
  1172. }
  1173. } else {
  1174. // Clear expired progress
  1175. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1176. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1177. }
  1178. }
  1179. // Process patterns in smaller batches to avoid overwhelming the server
  1180. const BATCH_SIZE = 10;
  1181. const remainingPatterns = uncachedPatterns.slice(startIndex);
  1182. const totalBatches = Math.ceil(remainingPatterns.length / BATCH_SIZE);
  1183. for (let i = 0; i < totalBatches; i++) {
  1184. const batchStart = i * BATCH_SIZE;
  1185. const batchEnd = Math.min(batchStart + BATCH_SIZE, remainingPatterns.length);
  1186. const batchPatterns = remainingPatterns.slice(batchStart, batchEnd);
  1187. // Update button text with progress
  1188. const overallProgress = Math.round(((startIndex + batchStart + BATCH_SIZE) / uncachedPatterns.length) * 100);
  1189. cacheAllButton.innerHTML = `
  1190. <div class="bg-white bg-opacity-30 rounded-full h-4 w-4 flex items-center justify-center">
  1191. <div class="bg-white rounded-full h-2 w-2"></div>
  1192. </div>
  1193. <span>Caching ${overallProgress}%</span>
  1194. `;
  1195. try {
  1196. const response = await fetch('/preview_thr_batch', {
  1197. method: 'POST',
  1198. headers: { 'Content-Type': 'application/json' },
  1199. body: JSON.stringify({ file_names: batchPatterns })
  1200. });
  1201. if (response.ok) {
  1202. const results = await response.json();
  1203. // Cache each preview
  1204. for (const [pattern, data] of Object.entries(results)) {
  1205. if (data && !data.error && data.image_data) {
  1206. previewCache.set(pattern, data);
  1207. await savePreviewToCache(pattern, data);
  1208. // Save progress after each successful pattern
  1209. localStorage.setItem(CACHE_PROGRESS_KEY, pattern);
  1210. localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
  1211. }
  1212. }
  1213. }
  1214. } catch (error) {
  1215. logMessage(`Error caching batch ${i + 1}: ${error.message}`, LOG_TYPE.ERROR);
  1216. // Don't clear progress on error - allows resuming from last successful pattern
  1217. }
  1218. // Small delay between batches to prevent overwhelming the server
  1219. await new Promise(resolve => setTimeout(resolve, 100));
  1220. }
  1221. // Clear progress after successful completion
  1222. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1223. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1224. // Show success message
  1225. showStatusMessage('All pattern previews have been cached!', 'success');
  1226. } catch (error) {
  1227. logMessage(`Error caching previews: ${error.message}`, LOG_TYPE.ERROR);
  1228. showStatusMessage('Failed to cache all previews. Click again to resume.', 'error');
  1229. } finally {
  1230. // Reset button state
  1231. if (cacheAllButton) {
  1232. cacheAllButton.disabled = false;
  1233. cacheAllButton.innerHTML = `
  1234. <span class="material-icons text-sm">cached</span>
  1235. Cache All Previews
  1236. `;
  1237. }
  1238. }
  1239. }
  1240. // Open animated preview modal
  1241. async function openAnimatedPreview(pattern) {
  1242. try {
  1243. const modal = document.getElementById('animatedPreviewModal');
  1244. const title = document.getElementById('animatedPreviewTitle');
  1245. const canvas = document.getElementById('animatedPreviewCanvas');
  1246. const ctx = canvas.getContext('2d');
  1247. // Set title
  1248. title.textContent = pattern.replace('.thr', '').split('/').pop();
  1249. // Show modal
  1250. modal.classList.remove('hidden');
  1251. // Load pattern coordinates
  1252. const response = await fetch('/get_theta_rho_coordinates', {
  1253. method: 'POST',
  1254. headers: { 'Content-Type': 'application/json' },
  1255. body: JSON.stringify({ file_name: pattern })
  1256. });
  1257. if (!response.ok) {
  1258. throw new Error(`HTTP error! status: ${response.status}`);
  1259. }
  1260. const data = await response.json();
  1261. if (data.error) {
  1262. throw new Error(data.error);
  1263. }
  1264. animatedPreviewData = data.coordinates;
  1265. // Setup canvas
  1266. setupAnimatedPreviewCanvas(ctx);
  1267. // Setup controls
  1268. setupAnimatedPreviewControls();
  1269. // Draw initial state
  1270. drawAnimatedPreview(ctx, 0);
  1271. // Auto-play the animation
  1272. setTimeout(() => {
  1273. playAnimation();
  1274. }, 100); // Small delay to ensure everything is set up
  1275. } catch (error) {
  1276. logMessage(`Error opening animated preview: ${error.message}`, LOG_TYPE.ERROR);
  1277. showStatusMessage('Failed to load pattern for animation', 'error');
  1278. }
  1279. }
  1280. // Setup animated preview canvas
  1281. function setupAnimatedPreviewCanvas(ctx) {
  1282. const canvas = ctx.canvas;
  1283. const size = canvas.width;
  1284. const center = size / 2;
  1285. const scale = (size / 2) - 30; // Slightly smaller to account for border
  1286. // Clear canvas with white background
  1287. ctx.fillStyle = '#ffffff';
  1288. ctx.fillRect(0, 0, size, size);
  1289. // Set drawing style for ultra-high quality lines
  1290. ctx.strokeStyle = '#000000';
  1291. ctx.lineWidth = 1; // Thinner line for higher resolution
  1292. ctx.lineCap = 'round';
  1293. ctx.lineJoin = 'round';
  1294. // Enable high quality rendering
  1295. ctx.imageSmoothingEnabled = true;
  1296. ctx.imageSmoothingQuality = 'high';
  1297. }
  1298. // Setup animated preview controls
  1299. function setupAnimatedPreviewControls() {
  1300. const modal = document.getElementById('animatedPreviewModal');
  1301. const closeBtn = document.getElementById('closeAnimatedPreview');
  1302. const playPauseBtn = document.getElementById('playPauseBtn');
  1303. const resetBtn = document.getElementById('resetBtn');
  1304. const speedSlider = document.getElementById('speedSlider');
  1305. const speedValue = document.getElementById('speedValue');
  1306. const progressSlider = document.getElementById('progressSlider');
  1307. const progressValue = document.getElementById('progressValue');
  1308. const canvas = document.getElementById('animatedPreviewCanvas');
  1309. const playPauseOverlay = document.getElementById('playPauseOverlay');
  1310. // Set responsive canvas size with ultra-high-DPI support
  1311. const setCanvasSize = () => {
  1312. const container = canvas.parentElement;
  1313. const modal = document.getElementById('animatedPreviewModal');
  1314. if (!container || !modal) return;
  1315. // Calculate available viewport space
  1316. const viewportWidth = window.innerWidth;
  1317. const viewportHeight = window.innerHeight;
  1318. // Calculate modal content area (95vh max height - header - padding)
  1319. const modalMaxHeight = viewportHeight * 0.95;
  1320. const headerHeight = 80; // Approximate header height with padding
  1321. const modalPadding = 48; // Modal padding (p-6 = 24px each side)
  1322. const availableHeight = modalMaxHeight - headerHeight - modalPadding;
  1323. // Calculate available width (max-w-4xl = 896px, but respect viewport)
  1324. const modalMaxWidth = Math.min(896, viewportWidth - 32); // Account for modal margin
  1325. const availableWidth = modalMaxWidth - modalPadding;
  1326. // Calculate ideal canvas size (use 80% of available space as requested)
  1327. const targetHeight = availableHeight * 0.8;
  1328. const targetWidth = availableWidth * 0.8;
  1329. // Use the smaller dimension to maintain square aspect ratio
  1330. let idealSize = Math.min(targetWidth, targetHeight);
  1331. // Cap at reasonable maximum and minimum
  1332. idealSize = Math.min(idealSize, 800); // Maximum size cap
  1333. idealSize = Math.max(idealSize, 200); // Minimum size
  1334. const displaySize = idealSize;
  1335. console.log('Canvas sizing:', {
  1336. viewport: `${viewportWidth}x${viewportHeight}`,
  1337. availableModal: `${availableWidth}x${availableHeight}`,
  1338. target80pct: `${targetWidth}x${targetHeight}`,
  1339. finalSize: displaySize
  1340. });
  1341. // Get device pixel ratio and multiply by 2 for higher resolution
  1342. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  1343. // Set the display size (CSS pixels) - use pixels, not percentage
  1344. canvas.style.width = displaySize + 'px';
  1345. canvas.style.height = displaySize + 'px';
  1346. // Set the actual canvas size (device pixels) - increased resolution
  1347. canvas.width = displaySize * pixelRatio;
  1348. canvas.height = displaySize * pixelRatio;
  1349. // Scale the context to match the increased pixel ratio
  1350. const ctx = canvas.getContext('2d', { alpha: false }); // Disable alpha for better performance
  1351. ctx.scale(pixelRatio, pixelRatio);
  1352. // Enable high quality rendering
  1353. ctx.imageSmoothingEnabled = true;
  1354. ctx.imageSmoothingQuality = 'high';
  1355. // Redraw with new size
  1356. if (animatedPreviewData) {
  1357. setupAnimatedPreviewCanvas(ctx);
  1358. drawAnimatedPreview(ctx, currentProgress / 100);
  1359. }
  1360. };
  1361. // Set initial size
  1362. setCanvasSize();
  1363. // Handle window resize with debouncing
  1364. let resizeTimeout;
  1365. window.addEventListener('resize', () => {
  1366. clearTimeout(resizeTimeout);
  1367. resizeTimeout = setTimeout(setCanvasSize, 16); // ~60fps update rate
  1368. });
  1369. // Close modal
  1370. closeBtn.onclick = closeAnimatedPreview;
  1371. modal.onclick = (e) => {
  1372. if (e.target === modal) closeAnimatedPreview();
  1373. };
  1374. // Play/Pause button
  1375. playPauseBtn.onclick = toggleAnimation;
  1376. // Reset button
  1377. resetBtn.onclick = resetAnimation;
  1378. // Speed slider
  1379. speedSlider.oninput = (e) => {
  1380. animationSpeed = parseFloat(e.target.value);
  1381. speedValue.textContent = `${animationSpeed}x`;
  1382. };
  1383. // Progress slider
  1384. progressSlider.oninput = (e) => {
  1385. currentProgress = parseFloat(e.target.value);
  1386. progressValue.textContent = `${currentProgress.toFixed(1)}%`;
  1387. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1388. if (isPlaying) {
  1389. // Pause animation when manually adjusting progress
  1390. toggleAnimation();
  1391. }
  1392. };
  1393. // Canvas click to play/pause
  1394. canvas.onclick = () => {
  1395. playPauseOverlay.style.opacity = '1';
  1396. setTimeout(() => {
  1397. playPauseOverlay.style.opacity = '0';
  1398. }, 200);
  1399. toggleAnimation();
  1400. };
  1401. // Keyboard shortcuts
  1402. document.addEventListener('keydown', (e) => {
  1403. if (modal.classList.contains('hidden')) return;
  1404. switch(e.code) {
  1405. case 'Space':
  1406. e.preventDefault();
  1407. toggleAnimation();
  1408. break;
  1409. case 'Escape':
  1410. closeAnimatedPreview();
  1411. break;
  1412. case 'ArrowLeft':
  1413. e.preventDefault();
  1414. currentProgress = Math.max(0, currentProgress - 5);
  1415. updateProgressUI();
  1416. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1417. break;
  1418. case 'ArrowRight':
  1419. e.preventDefault();
  1420. currentProgress = Math.min(100, currentProgress + 5);
  1421. updateProgressUI();
  1422. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1423. break;
  1424. }
  1425. });
  1426. }
  1427. // Draw animated preview
  1428. function drawAnimatedPreview(ctx, progress) {
  1429. if (!animatedPreviewData || animatedPreviewData.length === 0) return;
  1430. const canvas = ctx.canvas;
  1431. const pixelRatio = (window.devicePixelRatio || 1) * 2; // Match the increased ratio
  1432. const displayWidth = parseInt(canvas.style.width);
  1433. const displayHeight = parseInt(canvas.style.height);
  1434. const center = (canvas.width / pixelRatio) / 2;
  1435. const scale = ((canvas.width / pixelRatio) / 2) - 30;
  1436. // Clear canvas with white background
  1437. ctx.clearRect(0, 0, canvas.width, canvas.height);
  1438. // Calculate how many points to draw
  1439. const totalPoints = animatedPreviewData.length;
  1440. const pointsToDraw = Math.floor(totalPoints * progress);
  1441. if (pointsToDraw < 2) return;
  1442. // Draw the path with ultra-high quality settings
  1443. ctx.beginPath();
  1444. ctx.strokeStyle = '#000000';
  1445. ctx.lineWidth = 1; // Thinner line for higher resolution
  1446. ctx.lineCap = 'round';
  1447. ctx.lineJoin = 'round';
  1448. // Ensure sub-pixel alignment for ultra-high resolution
  1449. for (let i = 0; i < pointsToDraw; i++) {
  1450. const [theta, rho] = animatedPreviewData[i];
  1451. // Round to nearest 0.25 for even more precise lines
  1452. // Mirror both X and Y coordinates
  1453. const x = Math.round((center + rho * scale * Math.cos(theta)) * 4) / 4; // Changed minus to plus
  1454. const y = Math.round((center + rho * scale * Math.sin(theta)) * 4) / 4;
  1455. if (i === 0) {
  1456. ctx.moveTo(x, y);
  1457. } else {
  1458. ctx.lineTo(x, y);
  1459. }
  1460. }
  1461. ctx.stroke();
  1462. // Draw current position dot
  1463. if (pointsToDraw > 0) {
  1464. const [currentTheta, currentRho] = animatedPreviewData[pointsToDraw - 1];
  1465. const currentX = Math.round((center + currentRho * scale * Math.cos(currentTheta)) * 4) / 4; // Changed minus to plus
  1466. const currentY = Math.round((center + currentRho * scale * Math.sin(currentTheta)) * 4) / 4;
  1467. // Draw a filled circle at current position with anti-aliasing
  1468. ctx.fillStyle = '#ff4444'; // Red dot
  1469. ctx.beginPath();
  1470. ctx.arc(currentX, currentY, 6, 0, 2 * Math.PI); // Increased dot size
  1471. ctx.fill();
  1472. // Add a subtle white border
  1473. ctx.strokeStyle = '#ffffff';
  1474. ctx.lineWidth = 1.5;
  1475. ctx.stroke();
  1476. }
  1477. }
  1478. // Toggle animation play/pause
  1479. function toggleAnimation() {
  1480. if (isPlaying) {
  1481. pauseAnimation();
  1482. } else {
  1483. playAnimation();
  1484. }
  1485. }
  1486. // Play animation
  1487. function playAnimation() {
  1488. if (!animatedPreviewData) return;
  1489. isPlaying = true;
  1490. lastTimestamp = performance.now();
  1491. // Update UI
  1492. const playPauseBtn = document.getElementById('playPauseBtn');
  1493. const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
  1494. const playPauseBtnText = document.getElementById('playPauseBtnText');
  1495. if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'pause';
  1496. if (playPauseBtnText) playPauseBtnText.textContent = 'Pause';
  1497. // Start animation loop
  1498. animationFrameId = requestAnimationFrame(animate);
  1499. }
  1500. // Pause animation
  1501. function pauseAnimation() {
  1502. isPlaying = false;
  1503. // Update UI
  1504. const playPauseBtn = document.getElementById('playPauseBtn');
  1505. const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
  1506. const playPauseBtnText = document.getElementById('playPauseBtnText');
  1507. if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'play_arrow';
  1508. if (playPauseBtnText) playPauseBtnText.textContent = 'Play';
  1509. // Cancel animation frame
  1510. if (animationFrameId) {
  1511. cancelAnimationFrame(animationFrameId);
  1512. animationFrameId = null;
  1513. }
  1514. }
  1515. // Animation loop
  1516. function animate(timestamp) {
  1517. if (!isPlaying) return;
  1518. const deltaTime = timestamp - lastTimestamp;
  1519. const progressIncrement = (deltaTime / 1000) * animationSpeed * 2.0; // Much faster base speed
  1520. currentProgress = Math.min(100, currentProgress + progressIncrement);
  1521. // Update UI
  1522. updateProgressUI();
  1523. // Draw frame
  1524. const canvas = document.getElementById('animatedPreviewCanvas');
  1525. if (canvas) {
  1526. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1527. }
  1528. // Continue animation
  1529. if (currentProgress < 100) {
  1530. lastTimestamp = timestamp;
  1531. animationFrameId = requestAnimationFrame(animate);
  1532. } else {
  1533. // Animation complete
  1534. pauseAnimation();
  1535. }
  1536. }
  1537. // Reset animation
  1538. function resetAnimation() {
  1539. pauseAnimation();
  1540. currentProgress = 0;
  1541. updateProgressUI();
  1542. const canvas = document.getElementById('animatedPreviewCanvas');
  1543. drawAnimatedPreview(canvas.getContext('2d'), 0);
  1544. }
  1545. // Update progress UI
  1546. function updateProgressUI() {
  1547. const progressSlider = document.getElementById('progressSlider');
  1548. const progressValue = document.getElementById('progressValue');
  1549. progressSlider.value = currentProgress;
  1550. progressValue.textContent = `${currentProgress.toFixed(1)}%`;
  1551. }
  1552. // Close animated preview
  1553. function closeAnimatedPreview() {
  1554. pauseAnimation();
  1555. const modal = document.getElementById('animatedPreviewModal');
  1556. modal.classList.add('hidden');
  1557. // Clear data
  1558. animatedPreviewData = null;
  1559. currentProgress = 0;
  1560. animationSpeed = 1;
  1561. // Reset UI
  1562. const speedSlider = document.getElementById('speedSlider');
  1563. const speedValue = document.getElementById('speedValue');
  1564. const progressSlider = document.getElementById('progressSlider');
  1565. const progressValue = document.getElementById('progressValue');
  1566. speedSlider.value = 1;
  1567. speedValue.textContent = '1x';
  1568. progressSlider.value = 0;
  1569. progressValue.textContent = '0%';
  1570. }