index.js 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722
  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="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-500"></div>
  290. </div>
  291. <div class="absolute inset-0 flex items-center justify-center">
  292. <div class="text-xs text-slate-500 mt-12">${loadingText}</div>
  293. </div>
  294. `;
  295. }
  296. // Add to pending batch
  297. pendingPatterns.set(pattern, element);
  298. // Process batch immediately if it's full or if it's a new upload
  299. if (pendingPatterns.size >= LAZY_BATCH_SIZE || isNewUpload) {
  300. processPendingBatch();
  301. }
  302. }
  303. // Update preview element with smooth transition
  304. function updatePreviewElement(element, imageUrl) {
  305. const img = new Image();
  306. img.onload = () => {
  307. element.innerHTML = '';
  308. element.appendChild(img);
  309. img.className = 'w-full h-full object-contain transition-opacity duration-300';
  310. img.style.opacity = '0';
  311. requestAnimationFrame(() => {
  312. img.style.opacity = '1';
  313. });
  314. };
  315. img.src = imageUrl;
  316. img.alt = 'Pattern Preview';
  317. }
  318. // Process pending patterns in batches
  319. async function processPendingBatch() {
  320. if (pendingPatterns.size === 0) return;
  321. // Create a copy of current pending patterns and clear the original
  322. const currentBatch = new Map(pendingPatterns);
  323. pendingPatterns.clear();
  324. const patternsToLoad = Array.from(currentBatch.keys());
  325. try {
  326. logMessage(`Loading batch of ${patternsToLoad.length} pattern previews`, LOG_TYPE.DEBUG);
  327. const response = await fetch('/preview_thr_batch', {
  328. method: 'POST',
  329. headers: { 'Content-Type': 'application/json' },
  330. body: JSON.stringify({ file_names: patternsToLoad })
  331. });
  332. if (response.ok) {
  333. const results = await response.json();
  334. // Process all results
  335. for (const [pattern, data] of Object.entries(results)) {
  336. const element = currentBatch.get(pattern);
  337. if (data && !data.error && data.image_data) {
  338. // Cache in memory with size limit
  339. if (previewCache.size > 100) { // Limit cache size
  340. const oldestKey = previewCache.keys().next().value;
  341. previewCache.delete(oldestKey);
  342. }
  343. previewCache.set(pattern, data);
  344. // Save to IndexedDB cache for persistence
  345. await savePreviewToCache(pattern, data);
  346. if (element) {
  347. updatePreviewElement(element, data.image_data);
  348. }
  349. } else {
  350. handleLoadError(pattern, element, data?.error || 'Failed to load preview');
  351. }
  352. }
  353. }
  354. } catch (error) {
  355. logMessage(`Error loading preview batch: ${error.message}`, LOG_TYPE.ERROR);
  356. // Handle error for each pattern in batch
  357. for (const pattern of patternsToLoad) {
  358. const element = currentBatch.get(pattern);
  359. handleLoadError(pattern, element, error.message);
  360. }
  361. }
  362. }
  363. // Trigger preview loading for currently visible patterns
  364. function triggerPreviewLoadingForVisible() {
  365. // Get all pattern cards currently in the DOM
  366. const patternCards = document.querySelectorAll('.pattern-card');
  367. patternCards.forEach(card => {
  368. const pattern = card.dataset.pattern;
  369. const previewContainer = card.querySelector('.pattern-preview');
  370. // Check if this pattern needs preview loading
  371. if (pattern && !previewCache.has(pattern) && !pendingPatterns.has(pattern)) {
  372. // Add to batch for immediate loading
  373. addPatternToBatch(pattern, previewContainer);
  374. }
  375. });
  376. // Process any pending previews immediately
  377. if (pendingPatterns.size > 0) {
  378. processPendingBatch();
  379. }
  380. }
  381. // Load individual pattern preview (fallback when batch loading fails)
  382. async function loadIndividualPreview(pattern, element) {
  383. try {
  384. logMessage(`Loading individual preview for ${pattern}`, LOG_TYPE.DEBUG);
  385. const response = await fetch('/preview_thr_batch', {
  386. method: 'POST',
  387. headers: { 'Content-Type': 'application/json' },
  388. body: JSON.stringify({ file_names: [pattern] })
  389. });
  390. if (response.ok) {
  391. const results = await response.json();
  392. const data = results[pattern];
  393. if (data && !data.error && data.image_data) {
  394. // Cache in memory with size limit
  395. if (previewCache.size > 100) { // Limit cache size
  396. const oldestKey = previewCache.keys().next().value;
  397. previewCache.delete(oldestKey);
  398. }
  399. previewCache.set(pattern, data);
  400. // Save to IndexedDB cache for persistence
  401. await savePreviewToCache(pattern, data);
  402. if (element) {
  403. updatePreviewElement(element, data.image_data);
  404. }
  405. logMessage(`Individual preview loaded successfully for ${pattern}`, LOG_TYPE.DEBUG);
  406. } else {
  407. throw new Error(data?.error || 'Failed to load preview data');
  408. }
  409. } else {
  410. throw new Error(`HTTP error! status: ${response.status}`);
  411. }
  412. } catch (error) {
  413. logMessage(`Error loading individual preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
  414. // Continue with normal error handling
  415. handleLoadError(pattern, element, error.message);
  416. }
  417. }
  418. // Handle load errors with retry logic
  419. function handleLoadError(pattern, element, error) {
  420. const retryCount = element.dataset.retryCount || 0;
  421. const isNewUpload = element.dataset.isNewUpload === 'true';
  422. const hasTriedIndividual = element.dataset.hasTriedIndividual === 'true';
  423. // Use longer delays for newly uploaded patterns
  424. const retryDelay = isNewUpload ? RETRY_DELAY * 2 : RETRY_DELAY;
  425. const maxRetries = isNewUpload ? MAX_RETRIES * 2 : MAX_RETRIES;
  426. if (retryCount < maxRetries) {
  427. // Update retry count
  428. element.dataset.retryCount = parseInt(retryCount) + 1;
  429. // Determine retry strategy
  430. let retryStrategy = 'batch';
  431. if (retryCount >= 1 && !hasTriedIndividual) {
  432. // After first batch attempt fails, try individual loading
  433. retryStrategy = 'individual';
  434. element.dataset.hasTriedIndividual = 'true';
  435. }
  436. // Show retry message with different text for new uploads and retry strategies
  437. let retryText;
  438. if (isNewUpload) {
  439. retryText = retryStrategy === 'individual' ?
  440. `Trying individual load... (${retryCount + 1}/${maxRetries})` :
  441. `Generating preview... (${retryCount + 1}/${maxRetries})`;
  442. } else {
  443. retryText = retryStrategy === 'individual' ?
  444. `Trying individual load... (${retryCount + 1}/${maxRetries})` :
  445. `Retrying... (${retryCount + 1}/${maxRetries})`;
  446. }
  447. element.innerHTML = `
  448. <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
  449. <div class="text-xs text-slate-500 text-center">
  450. <div>${isNewUpload ? 'Processing new pattern' : 'Failed to load'}</div>
  451. <div>${retryText}</div>
  452. </div>
  453. </div>
  454. `;
  455. // Retry after delay with appropriate strategy
  456. setTimeout(() => {
  457. if (retryStrategy === 'individual') {
  458. loadIndividualPreview(pattern, element);
  459. } else {
  460. addPatternToBatch(pattern, element);
  461. }
  462. }, retryDelay);
  463. } else {
  464. // Show final error state
  465. element.innerHTML = `
  466. <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
  467. <div class="text-xs text-slate-500 text-center">
  468. <div>Failed to load</div>
  469. <div>Click to retry</div>
  470. </div>
  471. </div>
  472. `;
  473. // Add click handler for manual retry
  474. element.onclick = () => {
  475. element.dataset.retryCount = '0';
  476. element.dataset.hasTriedIndividual = 'false';
  477. addPatternToBatch(pattern, element);
  478. };
  479. }
  480. previewCache.set(pattern, { error: true });
  481. }
  482. // Load and display patterns
  483. async function loadPatterns(forceRefresh = false) {
  484. try {
  485. logMessage('Loading patterns...', LOG_TYPE.INFO);
  486. logMessage('Fetching fresh patterns list from server', LOG_TYPE.DEBUG);
  487. const response = await fetch('/list_theta_rho_files');
  488. const allFiles = await response.json();
  489. logMessage(`Received ${allFiles.length} files from server`, LOG_TYPE.INFO);
  490. // Filter for .thr files
  491. let patterns = allFiles.filter(file => file.endsWith('.thr'));
  492. logMessage(`Filtered to ${patterns.length} .thr files`, LOG_TYPE.INFO);
  493. if (forceRefresh) {
  494. showStatusMessage('Patterns list refreshed successfully', 'success');
  495. }
  496. // Sort patterns with custom_patterns on top and all alphabetically sorted
  497. const sortedPatterns = patterns.sort((a, b) => {
  498. const isCustomA = a.startsWith('custom_patterns/');
  499. const isCustomB = b.startsWith('custom_patterns/');
  500. if (isCustomA && !isCustomB) return -1;
  501. if (!isCustomA && isCustomB) return 1;
  502. return a.localeCompare(b);
  503. });
  504. allPatterns = sortedPatterns;
  505. currentBatch = 0;
  506. logMessage('Displaying initial batch of patterns...', LOG_TYPE.INFO);
  507. displayPatternBatch();
  508. logMessage('Initial batch loaded successfully.', LOG_TYPE.SUCCESS);
  509. } catch (error) {
  510. logMessage(`Error loading patterns: ${error.message}`, LOG_TYPE.ERROR);
  511. console.error('Full error:', error);
  512. showStatusMessage('Failed to load patterns', 'error');
  513. }
  514. }
  515. // Display a batch of patterns with improved initial load
  516. function displayPatternBatch() {
  517. const patternGrid = document.querySelector('.grid');
  518. if (!patternGrid) {
  519. logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
  520. return;
  521. }
  522. const start = currentBatch * BATCH_SIZE;
  523. const end = Math.min(start + BATCH_SIZE, allPatterns.length);
  524. const batchPatterns = allPatterns.slice(start, end);
  525. // Display batch patterns
  526. batchPatterns.forEach(pattern => {
  527. const patternCard = createPatternCard(pattern);
  528. patternGrid.appendChild(patternCard);
  529. });
  530. // If there are more patterns to load, set up the observer for the last few cards
  531. if (end < allPatterns.length) {
  532. const lastCards = Array.from(patternGrid.children).slice(-3); // Observe last 3 cards
  533. lastCards.forEach(card => {
  534. const observer = new IntersectionObserver((entries) => {
  535. if (entries[0].isIntersecting) {
  536. currentBatch++;
  537. displayPatternBatch();
  538. observer.disconnect();
  539. }
  540. }, {
  541. rootMargin: '200px 0px',
  542. threshold: 0.1
  543. });
  544. observer.observe(card);
  545. });
  546. }
  547. }
  548. // Create a pattern card element
  549. function createPatternCard(pattern) {
  550. const card = document.createElement('div');
  551. card.className = 'pattern-card flex flex-col items-center gap-3 bg-gray-50';
  552. card.dataset.pattern = pattern;
  553. // Create preview container with proper styling for loading indicator
  554. const previewContainer = document.createElement('div');
  555. previewContainer.className = 'w-32 h-32 rounded-full shadow-md relative pattern-preview group';
  556. previewContainer.dataset.pattern = pattern;
  557. // Add loading indicator
  558. previewContainer.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-500"></div></div>';
  559. // Add play button overlay (hidden by default, shown on hover)
  560. const playOverlay = document.createElement('div');
  561. playOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer';
  562. 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>';
  563. // Add click handler for play button (separate from card click)
  564. playOverlay.addEventListener('click', (e) => {
  565. e.stopPropagation(); // Prevent card selection
  566. openAnimatedPreview(pattern);
  567. });
  568. previewContainer.appendChild(playOverlay);
  569. // Create pattern name
  570. const patternName = document.createElement('p');
  571. patternName.className = 'text-gray-700 text-sm font-medium text-center truncate w-full';
  572. patternName.textContent = pattern.replace('.thr', '').split('/').pop();
  573. // Add click handler
  574. card.onclick = () => selectPattern(pattern, card);
  575. // Check if preview is already in cache
  576. const previewData = previewCache.get(pattern);
  577. if (previewData && !previewData.error && previewData.image_data) {
  578. updatePreviewElement(previewContainer, previewData.image_data);
  579. } else {
  580. // Start observing the preview container for lazy loading
  581. previewObserver.observe(previewContainer);
  582. }
  583. card.appendChild(previewContainer);
  584. card.appendChild(patternName);
  585. return card;
  586. }
  587. // Select a pattern
  588. function selectPattern(pattern, card) {
  589. // Remove selected class from all cards
  590. document.querySelectorAll('.pattern-card').forEach(c => {
  591. c.classList.remove('selected');
  592. });
  593. // Add selected class to clicked card
  594. card.classList.add('selected');
  595. // Show pattern preview
  596. showPatternPreview(pattern);
  597. }
  598. // Show pattern preview
  599. async function showPatternPreview(pattern) {
  600. try {
  601. // Check in-memory cache first
  602. let data = previewCache.get(pattern);
  603. // If not in cache, fetch it
  604. if (!data) {
  605. const response = await fetch('/preview_thr_batch', {
  606. method: 'POST',
  607. headers: { 'Content-Type': 'application/json' },
  608. body: JSON.stringify({ file_names: [pattern] })
  609. });
  610. if (!response.ok) {
  611. throw new Error(`HTTP error! status: ${response.status}`);
  612. }
  613. const results = await response.json();
  614. data = results[pattern];
  615. if (data && !data.error) {
  616. // Cache in memory
  617. previewCache.set(pattern, data);
  618. } else {
  619. throw new Error(data?.error || 'Failed to get preview data');
  620. }
  621. }
  622. const previewPanel = document.getElementById('patternPreviewPanel');
  623. const layoutContainer = document.querySelector('.layout-content-container');
  624. // Update preview content
  625. if (data.image_data) {
  626. document.getElementById('patternPreviewImage').src = data.image_data;
  627. }
  628. // Set pattern name in the preview panel
  629. const patternName = pattern.replace('.thr', '').split('/').pop();
  630. document.getElementById('patternPreviewTitle').textContent = patternName;
  631. // Format and display coordinates
  632. const formatCoordinate = (coord) => {
  633. if (!coord) return '(0, 0)';
  634. const x = coord.x !== undefined ? coord.x.toFixed(1) : '0.0';
  635. const y = coord.y !== undefined ? coord.y.toFixed(1) : '0.0';
  636. return `(${x}, ${y})`;
  637. };
  638. document.getElementById('firstCoordinate').textContent = formatCoordinate(data.first_coordinate);
  639. document.getElementById('lastCoordinate').textContent = formatCoordinate(data.last_coordinate);
  640. // Show preview panel
  641. previewPanel.classList.remove('translate-x-full');
  642. if (window.innerWidth >= 1024) {
  643. // For large screens, show preview alongside content
  644. layoutContainer.parentElement.classList.add('preview-open');
  645. previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
  646. } else {
  647. // For small screens, show preview as overlay
  648. layoutContainer.parentElement.classList.remove('preview-open');
  649. }
  650. // Setup preview panel events
  651. setupPreviewPanelEvents(pattern);
  652. } catch (error) {
  653. logMessage(`Error showing preview: ${error.message}`, LOG_TYPE.ERROR);
  654. }
  655. }
  656. function hidePatternPreview() {
  657. const previewPanel = document.getElementById('patternPreviewPanel');
  658. const layoutContainer = document.querySelector('.layout-content-container');
  659. previewPanel.classList.add('translate-x-full');
  660. if (window.innerWidth >= 1024) {
  661. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  662. }
  663. layoutContainer.parentElement.classList.remove('preview-open');
  664. }
  665. // Add window resize handler
  666. window.addEventListener('resize', () => {
  667. const previewPanel = document.getElementById('patternPreviewPanel');
  668. const layoutContainer = document.querySelector('.layout-content-container');
  669. if (window.innerWidth >= 1024) {
  670. if (!previewPanel.classList.contains('translate-x-full')) {
  671. layoutContainer.parentElement.classList.add('preview-open');
  672. previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
  673. }
  674. } else {
  675. layoutContainer.parentElement.classList.remove('preview-open');
  676. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  677. }
  678. });
  679. // Setup preview panel events
  680. function setupPreviewPanelEvents(pattern) {
  681. const panel = document.getElementById('patternPreviewPanel');
  682. const closeButton = document.getElementById('closePreviewPanel');
  683. const playButton = document.getElementById('playPattern');
  684. const deleteButton = document.getElementById('deletePattern');
  685. const preExecutionInputs = document.querySelectorAll('input[name="preExecutionAction"]');
  686. const previewPlayOverlay = document.getElementById('previewPlayOverlay');
  687. // Close panel when clicking the close button
  688. closeButton.onclick = () => {
  689. hidePatternPreview();
  690. // Remove selected state from all cards when closing
  691. document.querySelectorAll('.pattern-card').forEach(c => {
  692. c.classList.remove('selected');
  693. });
  694. };
  695. // Handle play button overlay click in preview panel
  696. if (previewPlayOverlay) {
  697. previewPlayOverlay.onclick = () => {
  698. openAnimatedPreview(pattern);
  699. };
  700. }
  701. // Handle play button click
  702. playButton.onclick = async () => {
  703. if (!pattern) {
  704. showStatusMessage('No pattern selected', 'error');
  705. return;
  706. }
  707. try {
  708. // Show the preview modal
  709. if (window.openPlayerPreviewModal) {
  710. window.openPlayerPreviewModal();
  711. }
  712. // Get the selected pre-execution action
  713. const preExecutionInput = document.querySelector('input[name="preExecutionAction"]:checked');
  714. const preExecution = preExecutionInput ? preExecutionInput.value : 'none';
  715. const response = await fetch('/run_theta_rho', {
  716. method: 'POST',
  717. headers: {
  718. 'Content-Type': 'application/json'
  719. },
  720. body: JSON.stringify({
  721. file_name: pattern,
  722. pre_execution: preExecution
  723. })
  724. });
  725. const data = await response.json();
  726. if (response.ok) {
  727. showStatusMessage(`Running pattern: ${pattern.split('/').pop()}`, 'success');
  728. hidePatternPreview();
  729. } else {
  730. let errorMsg = data.detail || 'Failed to run pattern';
  731. let errorType = 'error';
  732. // Handle specific error cases with appropriate messaging
  733. if (data.detail === 'Connection not established') {
  734. errorMsg = 'Please connect to the device before running a pattern';
  735. errorType = 'warning';
  736. } else if (response.status === 409) {
  737. errorMsg = 'Another pattern is already running. Please stop the current pattern first.';
  738. errorType = 'warning';
  739. } else if (response.status === 404) {
  740. errorMsg = 'Pattern file not found. Please refresh the page and try again.';
  741. errorType = 'error';
  742. } else if (response.status === 400) {
  743. errorMsg = 'Invalid request. Please check your settings and try again.';
  744. errorType = 'error';
  745. } else if (response.status === 500) {
  746. errorMsg = 'Server error. Please try again later.';
  747. errorType = 'error';
  748. }
  749. showStatusMessage(errorMsg, errorType);
  750. return;
  751. }
  752. } catch (error) {
  753. console.error('Error running pattern:', error);
  754. // Handle network errors specifically
  755. if (error.name === 'TypeError' && error.message.includes('fetch')) {
  756. showStatusMessage('Network error. Please check your connection and try again.', 'error');
  757. } else if (error.message && error.message.includes('409')) {
  758. showStatusMessage('Another pattern is already running', 'warning');
  759. } else if (error.message) {
  760. showStatusMessage(error.message, 'error');
  761. } else {
  762. showStatusMessage('Failed to run pattern', 'error');
  763. }
  764. }
  765. };
  766. // Handle delete button click
  767. deleteButton.onclick = async () => {
  768. if (!pattern.startsWith('custom_patterns/')) {
  769. logMessage('Cannot delete built-in patterns', LOG_TYPE.WARNING);
  770. showStatusMessage('Cannot delete built-in patterns', 'warning');
  771. return;
  772. }
  773. if (confirm('Are you sure you want to delete this pattern?')) {
  774. try {
  775. logMessage(`Deleting pattern: ${pattern}`, LOG_TYPE.INFO);
  776. const response = await fetch('/delete_theta_rho_file', {
  777. method: 'POST',
  778. headers: {
  779. 'Content-Type': 'application/json'
  780. },
  781. body: JSON.stringify({ file_name: pattern })
  782. });
  783. if (!response.ok) {
  784. throw new Error(`HTTP error! status: ${response.status}`);
  785. }
  786. const result = await response.json();
  787. if (result.success) {
  788. logMessage(`Pattern deleted successfully: ${pattern}`, LOG_TYPE.SUCCESS);
  789. showStatusMessage(`Pattern "${pattern.split('/').pop()}" deleted successfully`);
  790. // Remove the pattern card
  791. const selectedCard = document.querySelector('.pattern-card.selected');
  792. if (selectedCard) {
  793. selectedCard.remove();
  794. }
  795. // Close the preview panel
  796. const previewPanel = document.getElementById('patternPreviewPanel');
  797. const layoutContainer = document.querySelector('.layout-content-container');
  798. previewPanel.classList.add('translate-x-full');
  799. if (window.innerWidth >= 1024) {
  800. previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
  801. }
  802. layoutContainer.parentElement.classList.remove('preview-open');
  803. // Clear the preview panel content
  804. document.getElementById('patternPreviewImage').src = '';
  805. document.getElementById('patternPreviewTitle').textContent = 'Pattern Details';
  806. document.getElementById('firstCoordinate').textContent = '(0, 0)';
  807. document.getElementById('lastCoordinate').textContent = '(0, 0)';
  808. // Refresh the pattern list (force refresh since pattern was deleted)
  809. await loadPatterns(true);
  810. } else {
  811. throw new Error(result.error || 'Unknown error');
  812. }
  813. } catch (error) {
  814. logMessage(`Failed to delete pattern: ${error.message}`, LOG_TYPE.ERROR);
  815. showStatusMessage(`Failed to delete pattern: ${error.message}`, 'error');
  816. }
  817. }
  818. };
  819. // Handle pre-execution action changes
  820. preExecutionInputs.forEach(input => {
  821. input.onchange = () => {
  822. const action = input.parentElement.textContent.trim();
  823. logMessage(`Pre-execution action changed to: ${action}`, LOG_TYPE.INFO);
  824. };
  825. });
  826. }
  827. // Search patterns
  828. function searchPatterns(query) {
  829. if (!query) {
  830. // If search is empty, clear grid and show all patterns
  831. const patternGrid = document.querySelector('.grid');
  832. if (patternGrid) {
  833. patternGrid.innerHTML = '';
  834. }
  835. // Reset current batch and display from beginning
  836. currentBatch = 0;
  837. displayPatternBatch();
  838. return;
  839. }
  840. const searchInput = query.toLowerCase();
  841. const patternGrid = document.querySelector('.grid');
  842. if (!patternGrid) {
  843. logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
  844. return;
  845. }
  846. // Clear existing patterns
  847. patternGrid.innerHTML = '';
  848. // Filter patterns
  849. const filteredPatterns = allPatterns.filter(pattern =>
  850. pattern.toLowerCase().includes(searchInput)
  851. );
  852. // Display filtered patterns
  853. filteredPatterns.forEach(pattern => {
  854. const patternCard = createPatternCard(pattern);
  855. patternGrid.appendChild(patternCard);
  856. });
  857. // Give the browser a chance to render the cards
  858. requestAnimationFrame(() => {
  859. // Trigger preview loading for the search results
  860. triggerPreviewLoadingForVisible();
  861. });
  862. logMessage(`Showing ${filteredPatterns.length} patterns matching "${query}"`, LOG_TYPE.INFO);
  863. }
  864. // Filter patterns by category
  865. function filterPatternsByCategory(category) {
  866. // TODO: Implement category filtering logic
  867. logMessage(`Filtering patterns by category: ${category}`, LOG_TYPE.INFO);
  868. }
  869. // Filter patterns by tag
  870. function filterPatternsByTag(tag) {
  871. // TODO: Implement tag filtering logic
  872. logMessage(`Filtering patterns by tag: ${tag}`, LOG_TYPE.INFO);
  873. }
  874. // Initialize the patterns page
  875. document.addEventListener('DOMContentLoaded', async () => {
  876. try {
  877. logMessage('Initializing patterns page...', LOG_TYPE.DEBUG);
  878. // Initialize IndexedDB preview cache (shared with playlists page)
  879. await initPreviewCacheDB();
  880. // Setup upload event handlers
  881. setupUploadEventHandlers();
  882. // Initialize intersection observer for lazy loading
  883. initPreviewObserver();
  884. // Setup search functionality
  885. const searchInput = document.getElementById('patternSearch');
  886. const searchButton = document.getElementById('searchButton');
  887. const cacheAllButton = document.getElementById('cacheAllButton');
  888. if (searchInput && searchButton) {
  889. // Search on button click
  890. searchButton.addEventListener('click', () => {
  891. searchPatterns(searchInput.value.trim());
  892. });
  893. // Search on Enter key
  894. searchInput.addEventListener('keypress', (e) => {
  895. if (e.key === 'Enter') {
  896. searchPatterns(searchInput.value.trim());
  897. }
  898. });
  899. // Clear search when input is empty
  900. searchInput.addEventListener('input', (e) => {
  901. if (e.target.value.trim() === '') {
  902. searchPatterns('');
  903. }
  904. });
  905. }
  906. // Setup cache all button
  907. if (cacheAllButton) {
  908. cacheAllButton.addEventListener('click', () => cacheAllPreviews());
  909. }
  910. // Load patterns on page load
  911. await loadPatterns();
  912. logMessage('Patterns page initialized successfully', LOG_TYPE.SUCCESS);
  913. } catch (error) {
  914. logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
  915. }
  916. });
  917. function updateCurrentlyPlayingUI(status) {
  918. // Get all required DOM elements once
  919. const container = document.getElementById('currently-playing-container');
  920. const fileNameElement = document.getElementById('currently-playing-file');
  921. const progressBar = document.getElementById('play_progress');
  922. const progressText = document.getElementById('play_progress_text');
  923. const pausePlayButton = document.getElementById('pausePlayCurrent');
  924. const speedDisplay = document.getElementById('current_speed_display');
  925. const speedInput = document.getElementById('speedInput');
  926. // Check if all required elements exist
  927. if (!container || !fileNameElement || !progressBar || !progressText) {
  928. console.log('Required DOM elements not found:', {
  929. container: !!container,
  930. fileNameElement: !!fileNameElement,
  931. progressBar: !!progressBar,
  932. progressText: !!progressText
  933. });
  934. setTimeout(() => updateCurrentlyPlayingUI(status), 100);
  935. return;
  936. }
  937. // Update container visibility based on status
  938. if (status.current_file && status.is_running) {
  939. document.body.classList.add('playing');
  940. container.style.display = 'flex';
  941. } else {
  942. document.body.classList.remove('playing');
  943. container.style.display = 'none';
  944. }
  945. // Update file name display
  946. if (status.current_file) {
  947. const fileName = status.current_file.replace('./patterns/', '');
  948. fileNameElement.textContent = fileName;
  949. } else {
  950. fileNameElement.textContent = 'No pattern playing';
  951. }
  952. // Update next file display
  953. const nextFileElement = document.getElementById('next-file');
  954. if (nextFileElement) {
  955. if (status.playlist && status.playlist.next_file) {
  956. const nextFileName = status.playlist.next_file.replace('./patterns/', '');
  957. nextFileElement.textContent = `(Next: ${nextFileName})`;
  958. nextFileElement.style.display = 'block';
  959. } else {
  960. nextFileElement.style.display = 'none';
  961. }
  962. }
  963. // Update speed display and input if they exist
  964. if (status.speed) {
  965. if (speedDisplay) {
  966. speedDisplay.textContent = `Current Speed: ${status.speed}`;
  967. }
  968. if (speedInput) {
  969. speedInput.value = status.speed;
  970. }
  971. }
  972. // Update pattern preview if it's a new pattern
  973. // ... existing code ...
  974. }
  975. // Setup upload event handlers
  976. function setupUploadEventHandlers() {
  977. // Upload file input handler
  978. document.getElementById('patternFileInput').addEventListener('change', async function(e) {
  979. const file = e.target.files[0];
  980. if (!file) return;
  981. try {
  982. const formData = new FormData();
  983. formData.append('file', file);
  984. const response = await fetch('/upload_theta_rho', {
  985. method: 'POST',
  986. body: formData
  987. });
  988. const result = await response.json();
  989. if (result.success) {
  990. showStatusMessage(`Pattern "${file.name}" uploaded successfully`);
  991. // Clear any existing cache for this pattern to ensure fresh loading
  992. const newPatternPath = `custom_patterns/${file.name}`;
  993. previewCache.delete(newPatternPath);
  994. // Add a small delay to allow backend preview generation to complete
  995. await new Promise(resolve => setTimeout(resolve, 1000));
  996. // Refresh the pattern list (force refresh since new pattern was uploaded)
  997. await loadPatterns(true);
  998. // Clear the file input
  999. e.target.value = '';
  1000. // Trigger preview loading for newly uploaded patterns with extended retry
  1001. setTimeout(() => {
  1002. const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
  1003. if (newPatternCard) {
  1004. const previewContainer = newPatternCard.querySelector('.pattern-preview');
  1005. if (previewContainer) {
  1006. // Clear any existing retry count and force reload
  1007. previewContainer.dataset.retryCount = '0';
  1008. previewContainer.dataset.hasTriedIndividual = 'false';
  1009. previewContainer.dataset.isNewUpload = 'true';
  1010. addPatternToBatch(newPatternPath, previewContainer);
  1011. }
  1012. }
  1013. }, 500);
  1014. } else {
  1015. showStatusMessage(`Failed to upload pattern: ${result.error}`, 'error');
  1016. }
  1017. } catch (error) {
  1018. console.error('Error uploading pattern:', error);
  1019. showStatusMessage(`Error uploading pattern: ${error.message}`, 'error');
  1020. }
  1021. });
  1022. // Pattern deletion handler
  1023. const deleteModal = document.getElementById('deleteConfirmModal');
  1024. if (deleteModal) {
  1025. const confirmBtn = deleteModal.querySelector('#confirmDeleteBtn');
  1026. const cancelBtn = deleteModal.querySelector('#cancelDeleteBtn');
  1027. if (confirmBtn) {
  1028. confirmBtn.addEventListener('click', async () => {
  1029. const patternToDelete = confirmBtn.dataset.pattern;
  1030. if (patternToDelete) {
  1031. await deletePattern(patternToDelete);
  1032. // Force refresh after deletion
  1033. await loadPatterns(true);
  1034. }
  1035. deleteModal.classList.add('hidden');
  1036. });
  1037. }
  1038. if (cancelBtn) {
  1039. cancelBtn.addEventListener('click', () => {
  1040. deleteModal.classList.add('hidden');
  1041. });
  1042. }
  1043. }
  1044. }
  1045. // Cache all pattern previews
  1046. async function cacheAllPreviews() {
  1047. const cacheAllButton = document.getElementById('cacheAllButton');
  1048. if (!cacheAllButton) return;
  1049. try {
  1050. // Disable button and show loading state
  1051. cacheAllButton.disabled = true;
  1052. // Get current cache size
  1053. const currentSize = await getPreviewCacheSize();
  1054. const maxSize = MAX_CACHE_SIZE_BYTES || (200 * 1024 * 1024); // 200MB default
  1055. if (currentSize > maxSize) {
  1056. // Clear cache if it's too large
  1057. await clearPreviewCache();
  1058. // Also clear progress since we're starting fresh
  1059. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1060. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1061. }
  1062. // Get all patterns that aren't cached yet
  1063. const uncachedPatterns = allPatterns.filter(pattern => !previewCache.has(pattern));
  1064. if (uncachedPatterns.length === 0) {
  1065. showStatusMessage('All patterns are already cached!', 'info');
  1066. return;
  1067. }
  1068. // Check for existing progress
  1069. let startIndex = 0;
  1070. const savedProgress = localStorage.getItem(CACHE_PROGRESS_KEY);
  1071. const savedTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
  1072. if (savedProgress && savedTimestamp) {
  1073. const progressAge = Date.now() - parseInt(savedTimestamp);
  1074. if (progressAge < CACHE_PROGRESS_EXPIRY) {
  1075. const lastCachedPattern = savedProgress;
  1076. const lastIndex = uncachedPatterns.findIndex(p => p === lastCachedPattern);
  1077. if (lastIndex !== -1) {
  1078. startIndex = lastIndex + 1;
  1079. showStatusMessage('Resuming from previous progress...', 'info');
  1080. }
  1081. } else {
  1082. // Clear expired progress
  1083. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1084. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1085. }
  1086. }
  1087. // Process patterns in smaller batches to avoid overwhelming the server
  1088. const BATCH_SIZE = 10;
  1089. const remainingPatterns = uncachedPatterns.slice(startIndex);
  1090. const totalBatches = Math.ceil(remainingPatterns.length / BATCH_SIZE);
  1091. for (let i = 0; i < totalBatches; i++) {
  1092. const batchStart = i * BATCH_SIZE;
  1093. const batchEnd = Math.min(batchStart + BATCH_SIZE, remainingPatterns.length);
  1094. const batchPatterns = remainingPatterns.slice(batchStart, batchEnd);
  1095. // Update button text with progress
  1096. const overallProgress = Math.round(((startIndex + batchStart + BATCH_SIZE) / uncachedPatterns.length) * 100);
  1097. cacheAllButton.innerHTML = `
  1098. <div class="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
  1099. <span>Caching ${overallProgress}%</span>
  1100. `;
  1101. try {
  1102. const response = await fetch('/preview_thr_batch', {
  1103. method: 'POST',
  1104. headers: { 'Content-Type': 'application/json' },
  1105. body: JSON.stringify({ file_names: batchPatterns })
  1106. });
  1107. if (response.ok) {
  1108. const results = await response.json();
  1109. // Cache each preview
  1110. for (const [pattern, data] of Object.entries(results)) {
  1111. if (data && !data.error && data.image_data) {
  1112. previewCache.set(pattern, data);
  1113. await savePreviewToCache(pattern, data);
  1114. // Save progress after each successful pattern
  1115. localStorage.setItem(CACHE_PROGRESS_KEY, pattern);
  1116. localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
  1117. }
  1118. }
  1119. }
  1120. } catch (error) {
  1121. logMessage(`Error caching batch ${i + 1}: ${error.message}`, LOG_TYPE.ERROR);
  1122. // Don't clear progress on error - allows resuming from last successful pattern
  1123. }
  1124. // Small delay between batches to prevent overwhelming the server
  1125. await new Promise(resolve => setTimeout(resolve, 100));
  1126. }
  1127. // Clear progress after successful completion
  1128. localStorage.removeItem(CACHE_PROGRESS_KEY);
  1129. localStorage.removeItem(CACHE_TIMESTAMP_KEY);
  1130. // Show success message
  1131. showStatusMessage('All pattern previews have been cached!', 'success');
  1132. } catch (error) {
  1133. logMessage(`Error caching previews: ${error.message}`, LOG_TYPE.ERROR);
  1134. showStatusMessage('Failed to cache all previews. Click again to resume.', 'error');
  1135. } finally {
  1136. // Reset button state
  1137. if (cacheAllButton) {
  1138. cacheAllButton.disabled = false;
  1139. cacheAllButton.innerHTML = `
  1140. <span class="material-icons text-sm">cached</span>
  1141. Cache All Previews
  1142. `;
  1143. }
  1144. }
  1145. }
  1146. // Open animated preview modal
  1147. async function openAnimatedPreview(pattern) {
  1148. try {
  1149. const modal = document.getElementById('animatedPreviewModal');
  1150. const title = document.getElementById('animatedPreviewTitle');
  1151. const canvas = document.getElementById('animatedPreviewCanvas');
  1152. const ctx = canvas.getContext('2d');
  1153. // Set title
  1154. title.textContent = pattern.replace('.thr', '').split('/').pop();
  1155. // Show modal
  1156. modal.classList.remove('hidden');
  1157. // Load pattern coordinates
  1158. const response = await fetch('/get_theta_rho_coordinates', {
  1159. method: 'POST',
  1160. headers: { 'Content-Type': 'application/json' },
  1161. body: JSON.stringify({ file_name: pattern })
  1162. });
  1163. if (!response.ok) {
  1164. throw new Error(`HTTP error! status: ${response.status}`);
  1165. }
  1166. const data = await response.json();
  1167. if (data.error) {
  1168. throw new Error(data.error);
  1169. }
  1170. animatedPreviewData = data.coordinates;
  1171. // Setup canvas
  1172. setupAnimatedPreviewCanvas(ctx);
  1173. // Setup controls
  1174. setupAnimatedPreviewControls();
  1175. // Draw initial state
  1176. drawAnimatedPreview(ctx, 0);
  1177. // Auto-play the animation
  1178. setTimeout(() => {
  1179. playAnimation();
  1180. }, 100); // Small delay to ensure everything is set up
  1181. } catch (error) {
  1182. logMessage(`Error opening animated preview: ${error.message}`, LOG_TYPE.ERROR);
  1183. showStatusMessage('Failed to load pattern for animation', 'error');
  1184. }
  1185. }
  1186. // Setup animated preview canvas
  1187. function setupAnimatedPreviewCanvas(ctx) {
  1188. const canvas = ctx.canvas;
  1189. const size = canvas.width;
  1190. const center = size / 2;
  1191. const scale = (size / 2) - 30; // Slightly smaller to account for border
  1192. // Clear canvas with white background
  1193. ctx.fillStyle = '#ffffff';
  1194. ctx.fillRect(0, 0, size, size);
  1195. // Set drawing style for ultra-high quality lines
  1196. ctx.strokeStyle = '#000000';
  1197. ctx.lineWidth = 1; // Thinner line for higher resolution
  1198. ctx.lineCap = 'round';
  1199. ctx.lineJoin = 'round';
  1200. // Enable high quality rendering
  1201. ctx.imageSmoothingEnabled = true;
  1202. ctx.imageSmoothingQuality = 'high';
  1203. }
  1204. // Setup animated preview controls
  1205. function setupAnimatedPreviewControls() {
  1206. const modal = document.getElementById('animatedPreviewModal');
  1207. const closeBtn = document.getElementById('closeAnimatedPreview');
  1208. const playPauseBtn = document.getElementById('playPauseBtn');
  1209. const resetBtn = document.getElementById('resetBtn');
  1210. const speedSlider = document.getElementById('speedSlider');
  1211. const speedValue = document.getElementById('speedValue');
  1212. const progressSlider = document.getElementById('progressSlider');
  1213. const progressValue = document.getElementById('progressValue');
  1214. const canvas = document.getElementById('animatedPreviewCanvas');
  1215. const playPauseOverlay = document.getElementById('playPauseOverlay');
  1216. // Set responsive canvas size with ultra-high-DPI support
  1217. const setCanvasSize = () => {
  1218. const isMobile = window.innerWidth < 768;
  1219. const displaySize = isMobile ? Math.min(window.innerWidth - 80, 400) : 800;
  1220. // Get device pixel ratio and multiply by 2 for higher resolution
  1221. const pixelRatio = (window.devicePixelRatio || 1) * 2;
  1222. // Set the display size (CSS pixels)
  1223. canvas.style.width = displaySize + 'px';
  1224. canvas.style.height = displaySize + 'px';
  1225. // Set the actual canvas size (device pixels) - increased resolution
  1226. canvas.width = displaySize * pixelRatio;
  1227. canvas.height = displaySize * pixelRatio;
  1228. // Scale the context to match the increased pixel ratio
  1229. const ctx = canvas.getContext('2d', { alpha: false }); // Disable alpha for better performance
  1230. ctx.scale(pixelRatio, pixelRatio);
  1231. // Enable high quality rendering
  1232. ctx.imageSmoothingEnabled = true;
  1233. ctx.imageSmoothingQuality = 'high';
  1234. // Redraw with new size
  1235. if (animatedPreviewData) {
  1236. setupAnimatedPreviewCanvas(ctx);
  1237. drawAnimatedPreview(ctx, currentProgress / 100);
  1238. }
  1239. };
  1240. // Set initial size
  1241. setCanvasSize();
  1242. // Handle window resize
  1243. window.addEventListener('resize', setCanvasSize);
  1244. // Close modal
  1245. closeBtn.onclick = closeAnimatedPreview;
  1246. modal.onclick = (e) => {
  1247. if (e.target === modal) closeAnimatedPreview();
  1248. };
  1249. // Play/Pause button
  1250. playPauseBtn.onclick = toggleAnimation;
  1251. // Reset button
  1252. resetBtn.onclick = resetAnimation;
  1253. // Speed slider
  1254. speedSlider.oninput = (e) => {
  1255. animationSpeed = parseFloat(e.target.value);
  1256. speedValue.textContent = `${animationSpeed}x`;
  1257. };
  1258. // Progress slider
  1259. progressSlider.oninput = (e) => {
  1260. currentProgress = parseFloat(e.target.value);
  1261. progressValue.textContent = `${currentProgress.toFixed(1)}%`;
  1262. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1263. if (isPlaying) {
  1264. // Pause animation when manually adjusting progress
  1265. toggleAnimation();
  1266. }
  1267. };
  1268. // Canvas click to play/pause
  1269. canvas.onclick = () => {
  1270. playPauseOverlay.style.opacity = '1';
  1271. setTimeout(() => {
  1272. playPauseOverlay.style.opacity = '0';
  1273. }, 200);
  1274. toggleAnimation();
  1275. };
  1276. // Keyboard shortcuts
  1277. document.addEventListener('keydown', (e) => {
  1278. if (modal.classList.contains('hidden')) return;
  1279. switch(e.code) {
  1280. case 'Space':
  1281. e.preventDefault();
  1282. toggleAnimation();
  1283. break;
  1284. case 'Escape':
  1285. closeAnimatedPreview();
  1286. break;
  1287. case 'ArrowLeft':
  1288. e.preventDefault();
  1289. currentProgress = Math.max(0, currentProgress - 5);
  1290. updateProgressUI();
  1291. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1292. break;
  1293. case 'ArrowRight':
  1294. e.preventDefault();
  1295. currentProgress = Math.min(100, currentProgress + 5);
  1296. updateProgressUI();
  1297. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1298. break;
  1299. }
  1300. });
  1301. }
  1302. // Draw animated preview
  1303. function drawAnimatedPreview(ctx, progress) {
  1304. if (!animatedPreviewData || animatedPreviewData.length === 0) return;
  1305. const canvas = ctx.canvas;
  1306. const pixelRatio = (window.devicePixelRatio || 1) * 2; // Match the increased ratio
  1307. const displayWidth = parseInt(canvas.style.width);
  1308. const displayHeight = parseInt(canvas.style.height);
  1309. const center = (canvas.width / pixelRatio) / 2;
  1310. const scale = ((canvas.width / pixelRatio) / 2) - 30;
  1311. // Clear canvas with white background
  1312. ctx.clearRect(0, 0, canvas.width, canvas.height);
  1313. // Calculate how many points to draw
  1314. const totalPoints = animatedPreviewData.length;
  1315. const pointsToDraw = Math.floor(totalPoints * progress);
  1316. if (pointsToDraw < 2) return;
  1317. // Draw the path with ultra-high quality settings
  1318. ctx.beginPath();
  1319. ctx.strokeStyle = '#000000';
  1320. ctx.lineWidth = 1; // Thinner line for higher resolution
  1321. ctx.lineCap = 'round';
  1322. ctx.lineJoin = 'round';
  1323. // Ensure sub-pixel alignment for ultra-high resolution
  1324. for (let i = 0; i < pointsToDraw; i++) {
  1325. const [theta, rho] = animatedPreviewData[i];
  1326. // Round to nearest 0.25 for even more precise lines
  1327. // Mirror both X and Y coordinates
  1328. const x = Math.round((center + rho * scale * Math.cos(theta)) * 4) / 4; // Changed minus to plus
  1329. const y = Math.round((center + rho * scale * Math.sin(theta)) * 4) / 4;
  1330. if (i === 0) {
  1331. ctx.moveTo(x, y);
  1332. } else {
  1333. ctx.lineTo(x, y);
  1334. }
  1335. }
  1336. ctx.stroke();
  1337. // Draw current position dot
  1338. if (pointsToDraw > 0) {
  1339. const [currentTheta, currentRho] = animatedPreviewData[pointsToDraw - 1];
  1340. const currentX = Math.round((center + currentRho * scale * Math.cos(currentTheta)) * 4) / 4; // Changed minus to plus
  1341. const currentY = Math.round((center + currentRho * scale * Math.sin(currentTheta)) * 4) / 4;
  1342. // Draw a filled circle at current position with anti-aliasing
  1343. ctx.fillStyle = '#ff4444'; // Red dot
  1344. ctx.beginPath();
  1345. ctx.arc(currentX, currentY, 6, 0, 2 * Math.PI); // Increased dot size
  1346. ctx.fill();
  1347. // Add a subtle white border
  1348. ctx.strokeStyle = '#ffffff';
  1349. ctx.lineWidth = 1.5;
  1350. ctx.stroke();
  1351. }
  1352. }
  1353. // Toggle animation play/pause
  1354. function toggleAnimation() {
  1355. if (isPlaying) {
  1356. pauseAnimation();
  1357. } else {
  1358. playAnimation();
  1359. }
  1360. }
  1361. // Play animation
  1362. function playAnimation() {
  1363. if (!animatedPreviewData) return;
  1364. isPlaying = true;
  1365. lastTimestamp = performance.now();
  1366. // Update UI
  1367. const playPauseBtn = document.getElementById('playPauseBtn');
  1368. const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
  1369. const playPauseBtnText = document.getElementById('playPauseBtnText');
  1370. if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'pause';
  1371. if (playPauseBtnText) playPauseBtnText.textContent = 'Pause';
  1372. // Start animation loop
  1373. animationFrameId = requestAnimationFrame(animate);
  1374. }
  1375. // Pause animation
  1376. function pauseAnimation() {
  1377. isPlaying = false;
  1378. // Update UI
  1379. const playPauseBtn = document.getElementById('playPauseBtn');
  1380. const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
  1381. const playPauseBtnText = document.getElementById('playPauseBtnText');
  1382. if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'play_arrow';
  1383. if (playPauseBtnText) playPauseBtnText.textContent = 'Play';
  1384. // Cancel animation frame
  1385. if (animationFrameId) {
  1386. cancelAnimationFrame(animationFrameId);
  1387. animationFrameId = null;
  1388. }
  1389. }
  1390. // Animation loop
  1391. function animate(timestamp) {
  1392. if (!isPlaying) return;
  1393. const deltaTime = timestamp - lastTimestamp;
  1394. const progressIncrement = (deltaTime / 1000) * animationSpeed * 2.0; // Much faster base speed
  1395. currentProgress = Math.min(100, currentProgress + progressIncrement);
  1396. // Update UI
  1397. updateProgressUI();
  1398. // Draw frame
  1399. const canvas = document.getElementById('animatedPreviewCanvas');
  1400. if (canvas) {
  1401. drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
  1402. }
  1403. // Continue animation
  1404. if (currentProgress < 100) {
  1405. lastTimestamp = timestamp;
  1406. animationFrameId = requestAnimationFrame(animate);
  1407. } else {
  1408. // Animation complete
  1409. pauseAnimation();
  1410. }
  1411. }
  1412. // Reset animation
  1413. function resetAnimation() {
  1414. pauseAnimation();
  1415. currentProgress = 0;
  1416. updateProgressUI();
  1417. const canvas = document.getElementById('animatedPreviewCanvas');
  1418. drawAnimatedPreview(canvas.getContext('2d'), 0);
  1419. }
  1420. // Update progress UI
  1421. function updateProgressUI() {
  1422. const progressSlider = document.getElementById('progressSlider');
  1423. const progressValue = document.getElementById('progressValue');
  1424. progressSlider.value = currentProgress;
  1425. progressValue.textContent = `${currentProgress.toFixed(1)}%`;
  1426. }
  1427. // Close animated preview
  1428. function closeAnimatedPreview() {
  1429. pauseAnimation();
  1430. const modal = document.getElementById('animatedPreviewModal');
  1431. modal.classList.add('hidden');
  1432. // Clear data
  1433. animatedPreviewData = null;
  1434. currentProgress = 0;
  1435. animationSpeed = 1;
  1436. // Reset UI
  1437. const speedSlider = document.getElementById('speedSlider');
  1438. const speedValue = document.getElementById('speedValue');
  1439. const progressSlider = document.getElementById('progressSlider');
  1440. const progressValue = document.getElementById('progressValue');
  1441. speedSlider.value = 1;
  1442. speedValue.textContent = '1x';
  1443. progressSlider.value = 0;
  1444. progressValue.textContent = '0%';
  1445. }