index.js 67 KB

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