| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777 |
- // Global variables
- let allPatterns = [];
- let selectedPattern = null;
- let previewObserver = null;
- let currentBatch = 0;
- const BATCH_SIZE = 40; // Increased batch size for better performance
- let previewCache = new Map(); // Simple in-memory cache for preview data
- let imageCache = new Map(); // Cache for preloaded images
- // Global variables for lazy loading
- let pendingPatterns = new Map(); // pattern -> element mapping
- let batchTimeout = null;
- const INITIAL_BATCH_SIZE = 12; // Smaller initial batch for faster first load
- const LAZY_BATCH_SIZE = 5; // Reduced batch size for smoother loading
- const MAX_RETRIES = 3; // Maximum number of retries for failed loads
- const RETRY_DELAY = 1000; // Delay between retries in ms
- // Shared caching for patterns list (persistent across sessions)
- const PATTERNS_CACHE_KEY = 'dune_weaver_patterns_cache';
- // IndexedDB cache for preview images with size management (shared with playlists page)
- const PREVIEW_CACHE_DB_NAME = 'dune_weaver_previews';
- const PREVIEW_CACHE_DB_VERSION = 1;
- const PREVIEW_CACHE_STORE_NAME = 'previews';
- const MAX_CACHE_SIZE_MB = 200;
- const MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_MB * 1024 * 1024;
- let previewCacheDB = null;
- // Define constants for log message types
- const LOG_TYPE = {
- SUCCESS: 'success',
- WARNING: 'warning',
- ERROR: 'error',
- INFO: 'info',
- DEBUG: 'debug'
- };
- // Cache progress storage keys
- const CACHE_PROGRESS_KEY = 'dune_weaver_cache_progress';
- const CACHE_TIMESTAMP_KEY = 'dune_weaver_cache_timestamp';
- const CACHE_PROGRESS_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
- // Animated Preview Variables
- let animatedPreviewData = null;
- let animationFrameId = null;
- let isPlaying = false;
- let currentProgress = 0;
- let animationSpeed = 1;
- let lastTimestamp = 0;
- // Function to show status message
- function showStatusMessage(message, type = 'success') {
- const statusContainer = document.getElementById('status-message-container');
- const statusMessage = document.getElementById('status-message');
-
- if (!statusContainer || !statusMessage) return;
-
- // Set message and color based on type
- statusMessage.textContent = message;
- 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 ${
- type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' :
- type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' :
- type === 'warning' ? 'bg-yellow-50 text-yellow-700 border border-yellow-200' :
- 'bg-blue-50 text-blue-700 border border-blue-200'
- }`;
-
- // Show message with animation
- requestAnimationFrame(() => {
- statusMessage.classList.remove('opacity-0', '-translate-y-2');
- statusMessage.classList.add('opacity-100', 'translate-y-0');
- });
-
- // Hide message after 5 seconds
- setTimeout(() => {
- statusMessage.classList.remove('opacity-100', 'translate-y-0');
- statusMessage.classList.add('opacity-0', '-translate-y-2');
- }, 5000);
- }
- // Function to log messages
- function logMessage(message, type = LOG_TYPE.DEBUG) {
- console.log(`[${type}] ${message}`);
- }
- // Initialize IndexedDB for preview caching (shared with playlists page)
- async function initPreviewCacheDB() {
- if (previewCacheDB) return previewCacheDB;
-
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(PREVIEW_CACHE_DB_NAME, PREVIEW_CACHE_DB_VERSION);
-
- request.onerror = () => {
- logMessage('Failed to open preview cache database', LOG_TYPE.ERROR);
- reject(request.error);
- };
-
- request.onsuccess = () => {
- previewCacheDB = request.result;
- logMessage('Preview cache database opened successfully', LOG_TYPE.DEBUG);
- resolve(previewCacheDB);
- };
-
- request.onupgradeneeded = (event) => {
- const db = event.target.result;
-
- // Create object store for preview cache
- const store = db.createObjectStore(PREVIEW_CACHE_STORE_NAME, { keyPath: 'pattern' });
- store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
- store.createIndex('size', 'size', { unique: false });
-
- logMessage('Preview cache database schema created', LOG_TYPE.DEBUG);
- };
- });
- }
- // Get preview from IndexedDB cache
- async function getPreviewFromCache(pattern) {
- try {
- if (!previewCacheDB) await initPreviewCacheDB();
-
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-
- return new Promise((resolve, reject) => {
- const request = store.get(pattern);
-
- request.onsuccess = () => {
- const result = request.result;
- if (result) {
- // Update last accessed time
- result.lastAccessed = Date.now();
- store.put(result);
- resolve(result.data);
- } else {
- resolve(null);
- }
- };
-
- request.onerror = () => reject(request.error);
- });
- } catch (error) {
- logMessage(`Error getting preview from cache: ${error.message}`, LOG_TYPE.WARNING);
- return null;
- }
- }
- // Save preview to IndexedDB cache with size management
- async function savePreviewToCache(pattern, previewData) {
- try {
- if (!previewCacheDB) await initPreviewCacheDB();
-
- // Validate preview data before attempting to fetch
- if (!previewData || !previewData.image_data) {
- logMessage(`Invalid preview data for ${pattern}, skipping cache save`, LOG_TYPE.WARNING);
- return;
- }
-
- // Convert preview URL to blob for size calculation
- const response = await fetch(previewData.image_data);
- const blob = await response.blob();
- const size = blob.size;
-
- // Check if we need to free up space
- await managePreviewCacheSize(size);
-
- const cacheEntry = {
- pattern: pattern,
- data: previewData,
- size: size,
- lastAccessed: Date.now(),
- created: Date.now()
- };
-
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-
- return new Promise((resolve, reject) => {
- const request = store.put(cacheEntry);
-
- request.onsuccess = () => {
- logMessage(`Preview cached for ${pattern} (${(size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
- resolve();
- };
-
- request.onerror = () => reject(request.error);
- });
-
- } catch (error) {
- logMessage(`Error saving preview to cache: ${error.message}`, LOG_TYPE.WARNING);
- }
- }
- // Manage cache size by removing least recently used items
- async function managePreviewCacheSize(newItemSize) {
- try {
- const currentSize = await getPreviewCacheSize();
-
- if (currentSize + newItemSize <= MAX_CACHE_SIZE_BYTES) {
- return; // No cleanup needed
- }
-
- logMessage(`Cache size would exceed limit (${((currentSize + newItemSize) / 1024 / 1024).toFixed(1)}MB), cleaning up...`, LOG_TYPE.DEBUG);
-
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
- const index = store.index('lastAccessed');
-
- // Get all entries sorted by last accessed (oldest first)
- const entries = await new Promise((resolve, reject) => {
- const request = index.getAll();
- request.onsuccess = () => resolve(request.result);
- request.onerror = () => reject(request.error);
- });
-
- // Sort by last accessed time (oldest first)
- entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
-
- let freedSpace = 0;
- const targetSpace = newItemSize + (MAX_CACHE_SIZE_BYTES * 0.1); // Free 10% extra buffer
-
- for (const entry of entries) {
- if (freedSpace >= targetSpace) break;
-
- await new Promise((resolve, reject) => {
- const deleteRequest = store.delete(entry.pattern);
- deleteRequest.onsuccess = () => {
- freedSpace += entry.size;
- logMessage(`Evicted cached preview for ${entry.pattern} (${(entry.size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
- resolve();
- };
- deleteRequest.onerror = () => reject(deleteRequest.error);
- });
- }
-
- logMessage(`Freed ${(freedSpace / 1024 / 1024).toFixed(1)}MB from preview cache`, LOG_TYPE.DEBUG);
-
- } catch (error) {
- logMessage(`Error managing cache size: ${error.message}`, LOG_TYPE.WARNING);
- }
- }
- // Get current cache size
- async function getPreviewCacheSize() {
- try {
- if (!previewCacheDB) return 0;
-
- const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly');
- const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-
- return new Promise((resolve, reject) => {
- const request = store.getAll();
-
- request.onsuccess = () => {
- const totalSize = request.result.reduce((sum, entry) => sum + (entry.size || 0), 0);
- resolve(totalSize);
- };
-
- request.onerror = () => reject(request.error);
- });
-
- } catch (error) {
- logMessage(`Error getting cache size: ${error.message}`, LOG_TYPE.WARNING);
- return 0;
- }
- }
- // Preload images in batch
- async function preloadImages(urls) {
- const promises = urls.map(url => {
- return new Promise((resolve, reject) => {
- if (imageCache.has(url)) {
- resolve(imageCache.get(url));
- return;
- }
- const img = new Image();
- img.onload = () => {
- imageCache.set(url, img);
- resolve(img);
- };
- img.onerror = reject;
- img.src = url;
- });
- });
- return Promise.allSettled(promises);
- }
- // Initialize Intersection Observer for lazy loading
- function initPreviewObserver() {
- if (previewObserver) {
- previewObserver.disconnect();
- }
- previewObserver = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- const previewContainer = entry.target;
- const pattern = previewContainer.dataset.pattern;
- if (pattern) {
- addPatternToBatch(pattern, previewContainer);
- previewObserver.unobserve(previewContainer);
- }
- }
- });
- }, {
- rootMargin: '200px 0px', // Reduced margin for more precise loading
- threshold: 0.1
- });
- }
- // Add pattern to pending batch for efficient loading
- async function addPatternToBatch(pattern, element) {
- // Check in-memory cache first
- if (previewCache.has(pattern)) {
- const previewData = previewCache.get(pattern);
- if (previewData && !previewData.error) {
- if (element) {
- updatePreviewElement(element, previewData.image_data);
- }
- }
- return;
- }
- // Check IndexedDB cache
- const cachedData = await getPreviewFromCache(pattern);
- if (cachedData && !cachedData.error) {
- // Add to in-memory cache for faster access
- previewCache.set(pattern, cachedData);
- if (element) {
- updatePreviewElement(element, cachedData.image_data);
- }
- return;
- }
- // Check if this is a newly uploaded pattern
- const isNewUpload = element?.dataset.isNewUpload === 'true';
-
- // Reset retry flags when starting fresh
- if (element) {
- element.dataset.retryCount = '0';
- element.dataset.hasTriedIndividual = 'false';
- }
-
- // Add loading indicator with better styling
- if (!element.querySelector('img')) {
- const loadingText = isNewUpload ? 'Generating preview...' : 'Loading...';
- element.innerHTML = `
- <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
- <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>
- <div class="absolute inset-0 flex items-center justify-center">
- <div class="text-xs text-slate-500 mt-12">${loadingText}</div>
- </div>
- `;
- }
- // Add to pending batch
- pendingPatterns.set(pattern, element);
-
- // Process batch immediately if it's full or if it's a new upload
- if (pendingPatterns.size >= LAZY_BATCH_SIZE || isNewUpload) {
- processPendingBatch();
- }
- }
- // Update preview element with smooth transition
- function updatePreviewElement(element, imageUrl) {
- const img = new Image();
- img.onload = () => {
- element.innerHTML = '';
- element.appendChild(img);
- img.className = 'w-full h-full object-contain transition-opacity duration-300';
- img.style.opacity = '0';
- requestAnimationFrame(() => {
- img.style.opacity = '1';
- });
- };
- img.src = imageUrl;
- img.alt = 'Pattern Preview';
- }
- // Process pending patterns in batches
- async function processPendingBatch() {
- if (pendingPatterns.size === 0) return;
-
- // Create a copy of current pending patterns and clear the original
- const currentBatch = new Map(pendingPatterns);
- pendingPatterns.clear();
-
- const patternsToLoad = Array.from(currentBatch.keys());
-
- try {
- logMessage(`Loading batch of ${patternsToLoad.length} pattern previews`, LOG_TYPE.DEBUG);
-
- const response = await fetch('/preview_thr_batch', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ file_names: patternsToLoad })
- });
- if (response.ok) {
- const results = await response.json();
-
- // Process all results
- for (const [pattern, data] of Object.entries(results)) {
- const element = currentBatch.get(pattern);
-
- if (data && !data.error && data.image_data) {
- // Cache in memory with size limit
- if (previewCache.size > 100) { // Limit cache size
- const oldestKey = previewCache.keys().next().value;
- previewCache.delete(oldestKey);
- }
- previewCache.set(pattern, data);
-
- // Save to IndexedDB cache for persistence
- await savePreviewToCache(pattern, data);
-
- if (element) {
- updatePreviewElement(element, data.image_data);
- }
- } else {
- handleLoadError(pattern, element, data?.error || 'Failed to load preview');
- }
- }
- }
- } catch (error) {
- logMessage(`Error loading preview batch: ${error.message}`, LOG_TYPE.ERROR);
-
- // Handle error for each pattern in batch
- for (const pattern of patternsToLoad) {
- const element = currentBatch.get(pattern);
- handleLoadError(pattern, element, error.message);
- }
- }
- }
- // Trigger preview loading for currently visible patterns
- function triggerPreviewLoadingForVisible() {
- // Get all pattern cards currently in the DOM
- const patternCards = document.querySelectorAll('.pattern-card');
-
- patternCards.forEach(card => {
- const pattern = card.dataset.pattern;
- const previewContainer = card.querySelector('.pattern-preview');
-
- // Check if this pattern needs preview loading
- if (pattern && !previewCache.has(pattern) && !pendingPatterns.has(pattern)) {
- // Add to batch for immediate loading
- addPatternToBatch(pattern, previewContainer);
- }
- });
-
- // Process any pending previews immediately
- if (pendingPatterns.size > 0) {
- processPendingBatch();
- }
- }
- // Load individual pattern preview (fallback when batch loading fails)
- async function loadIndividualPreview(pattern, element) {
- try {
- logMessage(`Loading individual preview for ${pattern}`, LOG_TYPE.DEBUG);
-
- const response = await fetch('/preview_thr_batch', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ file_names: [pattern] })
- });
- if (response.ok) {
- const results = await response.json();
- const data = results[pattern];
-
- if (data && !data.error && data.image_data) {
- // Cache in memory with size limit
- if (previewCache.size > 100) { // Limit cache size
- const oldestKey = previewCache.keys().next().value;
- previewCache.delete(oldestKey);
- }
- previewCache.set(pattern, data);
-
- // Save to IndexedDB cache for persistence
- await savePreviewToCache(pattern, data);
-
- if (element) {
- updatePreviewElement(element, data.image_data);
- }
-
- logMessage(`Individual preview loaded successfully for ${pattern}`, LOG_TYPE.DEBUG);
- } else {
- throw new Error(data?.error || 'Failed to load preview data');
- }
- } else {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- } catch (error) {
- logMessage(`Error loading individual preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
- // Continue with normal error handling
- handleLoadError(pattern, element, error.message);
- }
- }
- // Handle load errors with retry logic
- function handleLoadError(pattern, element, error) {
- const retryCount = element.dataset.retryCount || 0;
- const isNewUpload = element.dataset.isNewUpload === 'true';
- const hasTriedIndividual = element.dataset.hasTriedIndividual === 'true';
-
- // Use longer delays for newly uploaded patterns
- const retryDelay = isNewUpload ? RETRY_DELAY * 2 : RETRY_DELAY;
- const maxRetries = isNewUpload ? MAX_RETRIES * 2 : MAX_RETRIES;
-
- if (retryCount < maxRetries) {
- // Update retry count
- element.dataset.retryCount = parseInt(retryCount) + 1;
-
- // Determine retry strategy
- let retryStrategy = 'batch';
- if (retryCount >= 1 && !hasTriedIndividual) {
- // After first batch attempt fails, try individual loading
- retryStrategy = 'individual';
- element.dataset.hasTriedIndividual = 'true';
- }
-
- // Show retry message with different text for new uploads and retry strategies
- let retryText;
- if (isNewUpload) {
- retryText = retryStrategy === 'individual' ?
- `Trying individual load... (${retryCount + 1}/${maxRetries})` :
- `Generating preview... (${retryCount + 1}/${maxRetries})`;
- } else {
- retryText = retryStrategy === 'individual' ?
- `Trying individual load... (${retryCount + 1}/${maxRetries})` :
- `Retrying... (${retryCount + 1}/${maxRetries})`;
- }
-
- element.innerHTML = `
- <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
- <div class="text-xs text-slate-500 text-center">
- <div>${isNewUpload ? 'Processing new pattern' : 'Failed to load'}</div>
- <div>${retryText}</div>
- </div>
- </div>
- `;
-
- // Retry after delay with appropriate strategy
- setTimeout(() => {
- if (retryStrategy === 'individual') {
- loadIndividualPreview(pattern, element);
- } else {
- addPatternToBatch(pattern, element);
- }
- }, retryDelay);
- } else {
- // Show final error state
- element.innerHTML = `
- <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
- <div class="text-xs text-slate-500 text-center">
- <div>Failed to load</div>
- <div>Click to retry</div>
- </div>
- </div>
- `;
-
- // Add click handler for manual retry
- element.onclick = () => {
- element.dataset.retryCount = '0';
- element.dataset.hasTriedIndividual = 'false';
- addPatternToBatch(pattern, element);
- };
- }
-
- previewCache.set(pattern, { error: true });
- }
- // Load and display patterns
- async function loadPatterns(forceRefresh = false) {
- try {
- logMessage('Loading patterns...', LOG_TYPE.INFO);
-
- logMessage('Fetching fresh patterns list from server', LOG_TYPE.DEBUG);
- const response = await fetch('/list_theta_rho_files');
- const allFiles = await response.json();
- logMessage(`Received ${allFiles.length} files from server`, LOG_TYPE.INFO);
- // Filter for .thr files
- let patterns = allFiles.filter(file => file.endsWith('.thr'));
- logMessage(`Filtered to ${patterns.length} .thr files`, LOG_TYPE.INFO);
- if (forceRefresh) {
- showStatusMessage('Patterns list refreshed successfully', 'success');
- }
-
- // Sort patterns with custom_patterns on top and all alphabetically sorted
- const sortedPatterns = patterns.sort((a, b) => {
- const isCustomA = a.startsWith('custom_patterns/');
- const isCustomB = b.startsWith('custom_patterns/');
- if (isCustomA && !isCustomB) return -1;
- if (!isCustomA && isCustomB) return 1;
- return a.localeCompare(b);
- });
- allPatterns = sortedPatterns;
- currentBatch = 0;
- logMessage('Displaying initial batch of patterns...', LOG_TYPE.INFO);
- displayPatternBatch();
- logMessage('Initial batch loaded successfully.', LOG_TYPE.SUCCESS);
- } catch (error) {
- logMessage(`Error loading patterns: ${error.message}`, LOG_TYPE.ERROR);
- console.error('Full error:', error);
- showStatusMessage('Failed to load patterns', 'error');
- }
- }
- // Display a batch of patterns with improved initial load
- function displayPatternBatch() {
- const patternGrid = document.querySelector('.grid');
- if (!patternGrid) {
- logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
- return;
- }
- const start = currentBatch * BATCH_SIZE;
- const end = Math.min(start + BATCH_SIZE, allPatterns.length);
- const batchPatterns = allPatterns.slice(start, end);
- // Display batch patterns
- batchPatterns.forEach(pattern => {
- const patternCard = createPatternCard(pattern);
- patternGrid.appendChild(patternCard);
- });
- // If there are more patterns to load, set up the observer for the last few cards
- if (end < allPatterns.length) {
- const lastCards = Array.from(patternGrid.children).slice(-3); // Observe last 3 cards
- lastCards.forEach(card => {
- const observer = new IntersectionObserver((entries) => {
- if (entries[0].isIntersecting) {
- currentBatch++;
- displayPatternBatch();
- observer.disconnect();
- }
- }, {
- rootMargin: '200px 0px',
- threshold: 0.1
- });
- observer.observe(card);
- });
- }
- }
- // Create a pattern card element
- function createPatternCard(pattern) {
- const card = document.createElement('div');
- card.className = 'pattern-card flex flex-col items-center gap-3 bg-gray-50';
- card.dataset.pattern = pattern;
-
- // Create preview container with proper styling for loading indicator
- const previewContainer = document.createElement('div');
- previewContainer.className = 'w-32 h-32 rounded-full shadow-md relative pattern-preview group';
- previewContainer.dataset.pattern = pattern;
-
- // Add loading indicator
- 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>';
-
- // Add play button overlay (hidden by default, shown on hover)
- const playOverlay = document.createElement('div');
- playOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer';
- 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>';
-
- // Add click handler for play button (separate from card click)
- playOverlay.addEventListener('click', (e) => {
- e.stopPropagation(); // Prevent card selection
- openAnimatedPreview(pattern);
- });
-
- previewContainer.appendChild(playOverlay);
-
- // Create pattern name
- const patternName = document.createElement('p');
- patternName.className = 'text-gray-700 text-sm font-medium text-center truncate w-full';
- patternName.textContent = pattern.replace('.thr', '').split('/').pop();
- // Add click handler
- card.onclick = () => selectPattern(pattern, card);
- // Check if preview is already in cache
- const previewData = previewCache.get(pattern);
- if (previewData && !previewData.error && previewData.image_data) {
- updatePreviewElement(previewContainer, previewData.image_data);
- } else {
- // Start observing the preview container for lazy loading
- previewObserver.observe(previewContainer);
- }
- card.appendChild(previewContainer);
- card.appendChild(patternName);
-
- return card;
- }
- // Select a pattern
- function selectPattern(pattern, card) {
- // Remove selected class from all cards
- document.querySelectorAll('.pattern-card').forEach(c => {
- c.classList.remove('selected');
- });
-
- // Add selected class to clicked card
- card.classList.add('selected');
-
- // Show pattern preview
- showPatternPreview(pattern);
- }
- // Show pattern preview
- async function showPatternPreview(pattern) {
- try {
- // Check in-memory cache first
- let data = previewCache.get(pattern);
-
- // If not in cache, fetch it
- if (!data) {
- const response = await fetch('/preview_thr_batch', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ file_names: [pattern] })
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const results = await response.json();
- data = results[pattern];
- if (data && !data.error) {
- // Cache in memory
- previewCache.set(pattern, data);
- } else {
- throw new Error(data?.error || 'Failed to get preview data');
- }
- }
- const previewPanel = document.getElementById('patternPreviewPanel');
- const layoutContainer = document.querySelector('.layout-content-container');
-
- // Update preview content
- if (data.image_data) {
- document.getElementById('patternPreviewImage').src = data.image_data;
- }
-
- // Set pattern name in the preview panel
- const patternName = pattern.replace('.thr', '').split('/').pop();
- document.getElementById('patternPreviewTitle').textContent = patternName;
- // Format and display coordinates
- const formatCoordinate = (coord) => {
- if (!coord) return '(0, 0)';
- const x = coord.x !== undefined ? coord.x.toFixed(1) : '0.0';
- const y = coord.y !== undefined ? coord.y.toFixed(1) : '0.0';
- return `(${x}, ${y})`;
- };
- document.getElementById('firstCoordinate').textContent = formatCoordinate(data.first_coordinate);
- document.getElementById('lastCoordinate').textContent = formatCoordinate(data.last_coordinate);
-
- // Show preview panel
- previewPanel.classList.remove('translate-x-full');
- if (window.innerWidth >= 1024) {
- // For large screens, show preview alongside content
- layoutContainer.parentElement.classList.add('preview-open');
- previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
- } else {
- // For small screens, show preview as overlay
- layoutContainer.parentElement.classList.remove('preview-open');
- }
- // Setup preview panel events
- setupPreviewPanelEvents(pattern);
- } catch (error) {
- logMessage(`Error showing preview: ${error.message}`, LOG_TYPE.ERROR);
- }
- }
- function hidePatternPreview() {
- const previewPanel = document.getElementById('patternPreviewPanel');
- const layoutContainer = document.querySelector('.layout-content-container');
-
- previewPanel.classList.add('translate-x-full');
- if (window.innerWidth >= 1024) {
- previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
- }
- layoutContainer.parentElement.classList.remove('preview-open');
- }
- // Add window resize handler
- window.addEventListener('resize', () => {
- const previewPanel = document.getElementById('patternPreviewPanel');
- const layoutContainer = document.querySelector('.layout-content-container');
-
- if (window.innerWidth >= 1024) {
- if (!previewPanel.classList.contains('translate-x-full')) {
- layoutContainer.parentElement.classList.add('preview-open');
- previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
- }
- } else {
- layoutContainer.parentElement.classList.remove('preview-open');
- previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
- }
- });
- // Setup preview panel events
- function setupPreviewPanelEvents(pattern) {
- const panel = document.getElementById('patternPreviewPanel');
- const closeButton = document.getElementById('closePreviewPanel');
- const playButton = document.getElementById('playPattern');
- const deleteButton = document.getElementById('deletePattern');
- const preExecutionInputs = document.querySelectorAll('input[name="preExecutionAction"]');
- const previewPlayOverlay = document.getElementById('previewPlayOverlay');
- // Close panel when clicking the close button
- closeButton.onclick = () => {
- hidePatternPreview();
- // Remove selected state from all cards when closing
- document.querySelectorAll('.pattern-card').forEach(c => {
- c.classList.remove('selected');
- });
- };
- // Handle play button overlay click in preview panel
- if (previewPlayOverlay) {
- previewPlayOverlay.onclick = () => {
- openAnimatedPreview(pattern);
- };
- }
- // Handle play button click
- playButton.onclick = async () => {
- if (!pattern) {
- showStatusMessage('No pattern selected', 'error');
- return;
- }
- try {
- // Show the preview modal
- if (window.openPlayerPreviewModal) {
- window.openPlayerPreviewModal();
- }
- // Get the selected pre-execution action
- const preExecutionInput = document.querySelector('input[name="preExecutionAction"]:checked');
- const preExecution = preExecutionInput ? preExecutionInput.value : 'none';
- const response = await fetch('/run_theta_rho', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- file_name: pattern,
- pre_execution: preExecution
- })
- });
- const data = await response.json();
- if (response.ok) {
- showStatusMessage(`Running pattern: ${pattern.split('/').pop()}`, 'success');
- hidePatternPreview();
-
- } else {
- let errorMsg = data.detail || 'Failed to run pattern';
- let errorType = 'error';
-
- // Handle specific error cases with appropriate messaging
- if (data.detail === 'Connection not established') {
- errorMsg = 'Please connect to the device before running a pattern';
- errorType = 'warning';
- } else if (response.status === 409) {
- errorMsg = 'Another pattern is already running. Please stop the current pattern first.';
- errorType = 'warning';
- } else if (response.status === 404) {
- errorMsg = 'Pattern file not found. Please refresh the page and try again.';
- errorType = 'error';
- } else if (response.status === 400) {
- errorMsg = 'Invalid request. Please check your settings and try again.';
- errorType = 'error';
- } else if (response.status === 500) {
- errorMsg = 'Server error. Please try again later.';
- errorType = 'error';
- }
-
- showStatusMessage(errorMsg, errorType);
- return;
- }
- } catch (error) {
- console.error('Error running pattern:', error);
-
- // Handle network errors specifically
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
- showStatusMessage('Network error. Please check your connection and try again.', 'error');
- } else if (error.message && error.message.includes('409')) {
- showStatusMessage('Another pattern is already running', 'warning');
- } else if (error.message) {
- showStatusMessage(error.message, 'error');
- } else {
- showStatusMessage('Failed to run pattern', 'error');
- }
- }
- };
- // Handle delete button click
- deleteButton.onclick = async () => {
- if (!pattern.startsWith('custom_patterns/')) {
- logMessage('Cannot delete built-in patterns', LOG_TYPE.WARNING);
- showStatusMessage('Cannot delete built-in patterns', 'warning');
- return;
- }
- if (confirm('Are you sure you want to delete this pattern?')) {
- try {
- logMessage(`Deleting pattern: ${pattern}`, LOG_TYPE.INFO);
- const response = await fetch('/delete_theta_rho_file', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ file_name: pattern })
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const result = await response.json();
- if (result.success) {
- logMessage(`Pattern deleted successfully: ${pattern}`, LOG_TYPE.SUCCESS);
- showStatusMessage(`Pattern "${pattern.split('/').pop()}" deleted successfully`);
- // Remove the pattern card
- const selectedCard = document.querySelector('.pattern-card.selected');
- if (selectedCard) {
- selectedCard.remove();
- }
- // Close the preview panel
- const previewPanel = document.getElementById('patternPreviewPanel');
- const layoutContainer = document.querySelector('.layout-content-container');
- previewPanel.classList.add('translate-x-full');
- if (window.innerWidth >= 1024) {
- previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
- }
- layoutContainer.parentElement.classList.remove('preview-open');
- // Clear the preview panel content
- document.getElementById('patternPreviewImage').src = '';
- document.getElementById('patternPreviewTitle').textContent = 'Pattern Details';
- document.getElementById('firstCoordinate').textContent = '(0, 0)';
- document.getElementById('lastCoordinate').textContent = '(0, 0)';
- // Refresh the pattern list (force refresh since pattern was deleted)
- await loadPatterns(true);
- } else {
- throw new Error(result.error || 'Unknown error');
- }
- } catch (error) {
- logMessage(`Failed to delete pattern: ${error.message}`, LOG_TYPE.ERROR);
- showStatusMessage(`Failed to delete pattern: ${error.message}`, 'error');
- }
- }
- };
- // Handle pre-execution action changes
- preExecutionInputs.forEach(input => {
- input.onchange = () => {
- const action = input.parentElement.textContent.trim();
- logMessage(`Pre-execution action changed to: ${action}`, LOG_TYPE.INFO);
- };
- });
- }
- // Search patterns
- function searchPatterns(query) {
- if (!query) {
- // If search is empty, clear grid and show all patterns
- const patternGrid = document.querySelector('.grid');
- if (patternGrid) {
- patternGrid.innerHTML = '';
- }
- // Reset current batch and display from beginning
- currentBatch = 0;
- displayPatternBatch();
- return;
- }
- const searchInput = query.toLowerCase();
- const patternGrid = document.querySelector('.grid');
- if (!patternGrid) {
- logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
- return;
- }
- // Clear existing patterns
- patternGrid.innerHTML = '';
-
- // Filter patterns
- const filteredPatterns = allPatterns.filter(pattern =>
- pattern.toLowerCase().includes(searchInput)
- );
- // Display filtered patterns
- filteredPatterns.forEach(pattern => {
- const patternCard = createPatternCard(pattern);
- patternGrid.appendChild(patternCard);
- });
- // Give the browser a chance to render the cards
- requestAnimationFrame(() => {
- // Trigger preview loading for the search results
- triggerPreviewLoadingForVisible();
- });
- logMessage(`Showing ${filteredPatterns.length} patterns matching "${query}"`, LOG_TYPE.INFO);
- }
- // Filter patterns by category
- function filterPatternsByCategory(category) {
- // TODO: Implement category filtering logic
- logMessage(`Filtering patterns by category: ${category}`, LOG_TYPE.INFO);
- }
- // Filter patterns by tag
- function filterPatternsByTag(tag) {
- // TODO: Implement tag filtering logic
- logMessage(`Filtering patterns by tag: ${tag}`, LOG_TYPE.INFO);
- }
- // Initialize the patterns page
- document.addEventListener('DOMContentLoaded', async () => {
- try {
- logMessage('Initializing patterns page...', LOG_TYPE.DEBUG);
-
- // Initialize IndexedDB preview cache (shared with playlists page)
- await initPreviewCacheDB();
-
- // Setup upload event handlers
- setupUploadEventHandlers();
-
- // Initialize intersection observer for lazy loading
- initPreviewObserver();
- // Setup search functionality
- const searchInput = document.getElementById('patternSearch');
- const searchButton = document.getElementById('searchButton');
- const cacheAllButton = document.getElementById('cacheAllButton');
-
- if (searchInput && searchButton) {
- // Search on button click
- searchButton.addEventListener('click', () => {
- searchPatterns(searchInput.value.trim());
- });
-
- // Search on Enter key
- searchInput.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') {
- searchPatterns(searchInput.value.trim());
- }
- });
-
- // Clear search when input is empty
- searchInput.addEventListener('input', (e) => {
- if (e.target.value.trim() === '') {
- searchPatterns('');
- }
- });
- }
- // Setup cache all button - now triggers the modal
- if (cacheAllButton) {
- cacheAllButton.addEventListener('click', () => {
- // Always show the modal when manually clicked, using forceShow parameter
- if (typeof showCacheAllPrompt === 'function') {
- showCacheAllPrompt(true); // true = forceShow
- } else {
- // Fallback if function not available
- const modal = document.getElementById('cacheAllPromptModal');
- if (modal) {
- modal.classList.remove('hidden');
- modal.dataset.manuallyTriggered = 'true';
- }
- }
- });
- }
- // Load patterns on page load
- await loadPatterns();
-
- logMessage('Patterns page initialized successfully', LOG_TYPE.SUCCESS);
- } catch (error) {
- logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
- }
- });
- function updateCurrentlyPlayingUI(status) {
- // Get all required DOM elements once
- const container = document.getElementById('currently-playing-container');
- const fileNameElement = document.getElementById('currently-playing-file');
- const progressBar = document.getElementById('play_progress');
- const progressText = document.getElementById('play_progress_text');
- const pausePlayButton = document.getElementById('pausePlayCurrent');
- const speedDisplay = document.getElementById('current_speed_display');
- const speedInput = document.getElementById('speedInput');
- // Check if all required elements exist
- if (!container || !fileNameElement || !progressBar || !progressText) {
- console.log('Required DOM elements not found:', {
- container: !!container,
- fileNameElement: !!fileNameElement,
- progressBar: !!progressBar,
- progressText: !!progressText
- });
- setTimeout(() => updateCurrentlyPlayingUI(status), 100);
- return;
- }
- // Update container visibility based on status
- if (status.current_file && status.is_running) {
- document.body.classList.add('playing');
- container.style.display = 'flex';
- } else {
- document.body.classList.remove('playing');
- container.style.display = 'none';
- }
- // Update file name display
- if (status.current_file) {
- const fileName = status.current_file.replace('./patterns/', '');
- fileNameElement.textContent = fileName;
- } else {
- fileNameElement.textContent = 'No pattern playing';
- }
- // Update next file display
- const nextFileElement = document.getElementById('next-file');
- if (nextFileElement) {
- if (status.playlist && status.playlist.next_file) {
- const nextFileName = status.playlist.next_file.replace('./patterns/', '');
- nextFileElement.textContent = `(Next: ${nextFileName})`;
- nextFileElement.style.display = 'block';
- } else {
- nextFileElement.style.display = 'none';
- }
- }
- // Update speed display and input if they exist
- if (status.speed) {
- if (speedDisplay) {
- speedDisplay.textContent = `Current Speed: ${status.speed}`;
- }
- if (speedInput) {
- speedInput.value = status.speed;
- }
- }
- // Update pattern preview if it's a new pattern
- // ... existing code ...
- }
- // Setup upload event handlers
- function setupUploadEventHandlers() {
- // Upload file input handler
- document.getElementById('patternFileInput').addEventListener('change', async function(e) {
- const file = e.target.files[0];
- if (!file) return;
- try {
- const formData = new FormData();
- formData.append('file', file);
- const response = await fetch('/upload_theta_rho', {
- method: 'POST',
- body: formData
- });
- const result = await response.json();
- if (result.success) {
- showStatusMessage(`Pattern "${file.name}" uploaded successfully`);
-
- // Clear any existing cache for this pattern to ensure fresh loading
- const newPatternPath = `custom_patterns/${file.name}`;
- previewCache.delete(newPatternPath);
-
- // Add a small delay to allow backend preview generation to complete
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- // Refresh the pattern list (force refresh since new pattern was uploaded)
- await loadPatterns(true);
-
- // Clear the file input
- e.target.value = '';
-
- // Trigger preview loading for newly uploaded patterns with extended retry
- setTimeout(() => {
- const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
- if (newPatternCard) {
- const previewContainer = newPatternCard.querySelector('.pattern-preview');
- if (previewContainer) {
- // Clear any existing retry count and force reload
- previewContainer.dataset.retryCount = '0';
- previewContainer.dataset.hasTriedIndividual = 'false';
- previewContainer.dataset.isNewUpload = 'true';
- addPatternToBatch(newPatternPath, previewContainer);
- }
- }
- }, 500);
- } else {
- showStatusMessage(`Failed to upload pattern: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('Error uploading pattern:', error);
- showStatusMessage(`Error uploading pattern: ${error.message}`, 'error');
- }
- });
- // Pattern deletion handler
- const deleteModal = document.getElementById('deleteConfirmModal');
- if (deleteModal) {
- const confirmBtn = deleteModal.querySelector('#confirmDeleteBtn');
- const cancelBtn = deleteModal.querySelector('#cancelDeleteBtn');
-
- if (confirmBtn) {
- confirmBtn.addEventListener('click', async () => {
- const patternToDelete = confirmBtn.dataset.pattern;
- if (patternToDelete) {
- await deletePattern(patternToDelete);
- // Force refresh after deletion
- await loadPatterns(true);
- }
- deleteModal.classList.add('hidden');
- });
- }
-
- if (cancelBtn) {
- cancelBtn.addEventListener('click', () => {
- deleteModal.classList.add('hidden');
- });
- }
- }
- }
- // Cache all pattern previews
- async function cacheAllPreviews() {
- const cacheAllButton = document.getElementById('cacheAllButton');
- if (!cacheAllButton) return;
- try {
- // Disable button and show loading state
- cacheAllButton.disabled = true;
- // Get current cache size
- const currentSize = await getPreviewCacheSize();
- const maxSize = MAX_CACHE_SIZE_BYTES || (200 * 1024 * 1024); // 200MB default
- if (currentSize > maxSize) {
- // Clear cache if it's too large
- await clearPreviewCache();
- // Also clear progress since we're starting fresh
- localStorage.removeItem(CACHE_PROGRESS_KEY);
- localStorage.removeItem(CACHE_TIMESTAMP_KEY);
- }
- // Get all patterns that aren't cached yet
- const uncachedPatterns = allPatterns.filter(pattern => !previewCache.has(pattern));
-
- if (uncachedPatterns.length === 0) {
- showStatusMessage('All patterns are already cached!', 'info');
- return;
- }
- // Check for existing progress
- let startIndex = 0;
- const savedProgress = localStorage.getItem(CACHE_PROGRESS_KEY);
- const savedTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
-
- if (savedProgress && savedTimestamp) {
- const progressAge = Date.now() - parseInt(savedTimestamp);
- if (progressAge < CACHE_PROGRESS_EXPIRY) {
- const lastCachedPattern = savedProgress;
- const lastIndex = uncachedPatterns.findIndex(p => p === lastCachedPattern);
- if (lastIndex !== -1) {
- startIndex = lastIndex + 1;
- showStatusMessage('Resuming from previous progress...', 'info');
- }
- } else {
- // Clear expired progress
- localStorage.removeItem(CACHE_PROGRESS_KEY);
- localStorage.removeItem(CACHE_TIMESTAMP_KEY);
- }
- }
- // Process patterns in smaller batches to avoid overwhelming the server
- const BATCH_SIZE = 10;
- const remainingPatterns = uncachedPatterns.slice(startIndex);
- const totalBatches = Math.ceil(remainingPatterns.length / BATCH_SIZE);
-
- for (let i = 0; i < totalBatches; i++) {
- const batchStart = i * BATCH_SIZE;
- const batchEnd = Math.min(batchStart + BATCH_SIZE, remainingPatterns.length);
- const batchPatterns = remainingPatterns.slice(batchStart, batchEnd);
-
- // Update button text with progress
- const overallProgress = Math.round(((startIndex + batchStart + BATCH_SIZE) / uncachedPatterns.length) * 100);
- cacheAllButton.innerHTML = `
- <div class="bg-white bg-opacity-30 rounded-full h-4 w-4 flex items-center justify-center">
- <div class="bg-white rounded-full h-2 w-2"></div>
- </div>
- <span>Caching ${overallProgress}%</span>
- `;
- try {
- const response = await fetch('/preview_thr_batch', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ file_names: batchPatterns })
- });
- if (response.ok) {
- const results = await response.json();
-
- // Cache each preview
- for (const [pattern, data] of Object.entries(results)) {
- if (data && !data.error && data.image_data) {
- previewCache.set(pattern, data);
- await savePreviewToCache(pattern, data);
-
- // Save progress after each successful pattern
- localStorage.setItem(CACHE_PROGRESS_KEY, pattern);
- localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
- }
- }
- }
- } catch (error) {
- logMessage(`Error caching batch ${i + 1}: ${error.message}`, LOG_TYPE.ERROR);
- // Don't clear progress on error - allows resuming from last successful pattern
- }
- // Small delay between batches to prevent overwhelming the server
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- // Clear progress after successful completion
- localStorage.removeItem(CACHE_PROGRESS_KEY);
- localStorage.removeItem(CACHE_TIMESTAMP_KEY);
- // Show success message
- showStatusMessage('All pattern previews have been cached!', 'success');
- } catch (error) {
- logMessage(`Error caching previews: ${error.message}`, LOG_TYPE.ERROR);
- showStatusMessage('Failed to cache all previews. Click again to resume.', 'error');
- } finally {
- // Reset button state
- if (cacheAllButton) {
- cacheAllButton.disabled = false;
- cacheAllButton.innerHTML = `
- <span class="material-icons text-sm">cached</span>
- Cache All Previews
- `;
- }
- }
- }
- // Open animated preview modal
- async function openAnimatedPreview(pattern) {
- try {
- const modal = document.getElementById('animatedPreviewModal');
- const title = document.getElementById('animatedPreviewTitle');
- const canvas = document.getElementById('animatedPreviewCanvas');
- const ctx = canvas.getContext('2d');
-
- // Set title
- title.textContent = pattern.replace('.thr', '').split('/').pop();
-
- // Show modal
- modal.classList.remove('hidden');
-
- // Load pattern coordinates
- const response = await fetch('/get_theta_rho_coordinates', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ file_name: pattern })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- if (data.error) {
- throw new Error(data.error);
- }
-
- animatedPreviewData = data.coordinates;
-
- // Setup canvas
- setupAnimatedPreviewCanvas(ctx);
-
- // Setup controls
- setupAnimatedPreviewControls();
-
- // Draw initial state
- drawAnimatedPreview(ctx, 0);
-
- // Auto-play the animation
- setTimeout(() => {
- playAnimation();
- }, 100); // Small delay to ensure everything is set up
-
- } catch (error) {
- logMessage(`Error opening animated preview: ${error.message}`, LOG_TYPE.ERROR);
- showStatusMessage('Failed to load pattern for animation', 'error');
- }
- }
- // Setup animated preview canvas
- function setupAnimatedPreviewCanvas(ctx) {
- const canvas = ctx.canvas;
- const size = canvas.width;
- const center = size / 2;
- const scale = (size / 2) - 30; // Slightly smaller to account for border
-
- // Clear canvas with white background
- ctx.fillStyle = '#ffffff';
- ctx.fillRect(0, 0, size, size);
-
- // Set drawing style for ultra-high quality lines
- ctx.strokeStyle = '#000000';
- ctx.lineWidth = 1; // Thinner line for higher resolution
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
-
- // Enable high quality rendering
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
- }
- // Setup animated preview controls
- function setupAnimatedPreviewControls() {
- const modal = document.getElementById('animatedPreviewModal');
- const closeBtn = document.getElementById('closeAnimatedPreview');
- const playPauseBtn = document.getElementById('playPauseBtn');
- const resetBtn = document.getElementById('resetBtn');
- const speedSlider = document.getElementById('speedSlider');
- const speedValue = document.getElementById('speedValue');
- const progressSlider = document.getElementById('progressSlider');
- const progressValue = document.getElementById('progressValue');
- const canvas = document.getElementById('animatedPreviewCanvas');
- const playPauseOverlay = document.getElementById('playPauseOverlay');
-
- // Set responsive canvas size with ultra-high-DPI support
- const setCanvasSize = () => {
- const container = canvas.parentElement;
- const modal = document.getElementById('animatedPreviewModal');
- if (!container || !modal) return;
-
- // Calculate available viewport space
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- // Calculate modal content area (95vh max height - header - padding)
- const modalMaxHeight = viewportHeight * 0.95;
- const headerHeight = 80; // Approximate header height with padding
- const modalPadding = 48; // Modal padding (p-6 = 24px each side)
- const availableHeight = modalMaxHeight - headerHeight - modalPadding;
-
- // Calculate available width (max-w-4xl = 896px, but respect viewport)
- const modalMaxWidth = Math.min(896, viewportWidth - 32); // Account for modal margin
- const availableWidth = modalMaxWidth - modalPadding;
-
- // Calculate ideal canvas size (use 80% of available space as requested)
- const targetHeight = availableHeight * 0.8;
- const targetWidth = availableWidth * 0.8;
-
- // Use the smaller dimension to maintain square aspect ratio
- let idealSize = Math.min(targetWidth, targetHeight);
-
- // Cap at reasonable maximum and minimum
- idealSize = Math.min(idealSize, 800); // Maximum size cap
- idealSize = Math.max(idealSize, 200); // Minimum size
-
- const displaySize = idealSize;
-
- console.log('Canvas sizing:', {
- viewport: `${viewportWidth}x${viewportHeight}`,
- availableModal: `${availableWidth}x${availableHeight}`,
- target80pct: `${targetWidth}x${targetHeight}`,
- finalSize: displaySize
- });
-
- // Get device pixel ratio and multiply by 2 for higher resolution
- const pixelRatio = (window.devicePixelRatio || 1) * 2;
-
- // Set the display size (CSS pixels) - use pixels, not percentage
- canvas.style.width = displaySize + 'px';
- canvas.style.height = displaySize + 'px';
-
- // Set the actual canvas size (device pixels) - increased resolution
- canvas.width = displaySize * pixelRatio;
- canvas.height = displaySize * pixelRatio;
-
- // Scale the context to match the increased pixel ratio
- const ctx = canvas.getContext('2d', { alpha: false }); // Disable alpha for better performance
- ctx.scale(pixelRatio, pixelRatio);
-
- // Enable high quality rendering
- ctx.imageSmoothingEnabled = true;
- ctx.imageSmoothingQuality = 'high';
-
- // Redraw with new size
- if (animatedPreviewData) {
- setupAnimatedPreviewCanvas(ctx);
- drawAnimatedPreview(ctx, currentProgress / 100);
- }
- };
-
- // Set initial size
- setCanvasSize();
-
- // Handle window resize with debouncing
- let resizeTimeout;
- window.addEventListener('resize', () => {
- clearTimeout(resizeTimeout);
- resizeTimeout = setTimeout(setCanvasSize, 16); // ~60fps update rate
- });
-
- // Close modal
- closeBtn.onclick = closeAnimatedPreview;
- modal.onclick = (e) => {
- if (e.target === modal) closeAnimatedPreview();
- };
-
- // Play/Pause button
- playPauseBtn.onclick = toggleAnimation;
-
- // Reset button
- resetBtn.onclick = resetAnimation;
-
- // Speed slider
- speedSlider.oninput = (e) => {
- animationSpeed = parseFloat(e.target.value);
- speedValue.textContent = `${animationSpeed}x`;
- };
-
- // Progress slider
- progressSlider.oninput = (e) => {
- currentProgress = parseFloat(e.target.value);
- progressValue.textContent = `${currentProgress.toFixed(1)}%`;
- drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
- if (isPlaying) {
- // Pause animation when manually adjusting progress
- toggleAnimation();
- }
- };
-
- // Canvas click to play/pause
- canvas.onclick = () => {
- playPauseOverlay.style.opacity = '1';
- setTimeout(() => {
- playPauseOverlay.style.opacity = '0';
- }, 200);
- toggleAnimation();
- };
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- if (modal.classList.contains('hidden')) return;
-
- switch(e.code) {
- case 'Space':
- e.preventDefault();
- toggleAnimation();
- break;
- case 'Escape':
- closeAnimatedPreview();
- break;
- case 'ArrowLeft':
- e.preventDefault();
- currentProgress = Math.max(0, currentProgress - 5);
- updateProgressUI();
- drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
- break;
- case 'ArrowRight':
- e.preventDefault();
- currentProgress = Math.min(100, currentProgress + 5);
- updateProgressUI();
- drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
- break;
- }
- });
- }
- // Draw animated preview
- function drawAnimatedPreview(ctx, progress) {
- if (!animatedPreviewData || animatedPreviewData.length === 0) return;
-
- const canvas = ctx.canvas;
- const pixelRatio = (window.devicePixelRatio || 1) * 2; // Match the increased ratio
- const displayWidth = parseInt(canvas.style.width);
- const displayHeight = parseInt(canvas.style.height);
- const center = (canvas.width / pixelRatio) / 2;
- const scale = ((canvas.width / pixelRatio) / 2) - 30;
-
- // Clear canvas with white background
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- // Calculate how many points to draw
- const totalPoints = animatedPreviewData.length;
- const pointsToDraw = Math.floor(totalPoints * progress);
-
- if (pointsToDraw < 2) return;
-
- // Draw the path with ultra-high quality settings
- ctx.beginPath();
- ctx.strokeStyle = '#000000';
- ctx.lineWidth = 1; // Thinner line for higher resolution
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
-
- // Ensure sub-pixel alignment for ultra-high resolution
- for (let i = 0; i < pointsToDraw; i++) {
- const [theta, rho] = animatedPreviewData[i];
- // Round to nearest 0.25 for even more precise lines
- // Mirror both X and Y coordinates
- const x = Math.round((center + rho * scale * Math.cos(theta)) * 4) / 4; // Changed minus to plus
- const y = Math.round((center + rho * scale * Math.sin(theta)) * 4) / 4;
-
- if (i === 0) {
- ctx.moveTo(x, y);
- } else {
- ctx.lineTo(x, y);
- }
- }
- ctx.stroke();
-
- // Draw current position dot
- if (pointsToDraw > 0) {
- const [currentTheta, currentRho] = animatedPreviewData[pointsToDraw - 1];
- const currentX = Math.round((center + currentRho * scale * Math.cos(currentTheta)) * 4) / 4; // Changed minus to plus
- const currentY = Math.round((center + currentRho * scale * Math.sin(currentTheta)) * 4) / 4;
-
- // Draw a filled circle at current position with anti-aliasing
- ctx.fillStyle = '#ff4444'; // Red dot
- ctx.beginPath();
- ctx.arc(currentX, currentY, 6, 0, 2 * Math.PI); // Increased dot size
- ctx.fill();
-
- // Add a subtle white border
- ctx.strokeStyle = '#ffffff';
- ctx.lineWidth = 1.5;
- ctx.stroke();
- }
- }
- // Toggle animation play/pause
- function toggleAnimation() {
- if (isPlaying) {
- pauseAnimation();
- } else {
- playAnimation();
- }
- }
- // Play animation
- function playAnimation() {
- if (!animatedPreviewData) return;
-
- isPlaying = true;
- lastTimestamp = performance.now();
-
- // Update UI
- const playPauseBtn = document.getElementById('playPauseBtn');
- const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
- const playPauseBtnText = document.getElementById('playPauseBtnText');
-
- if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'pause';
- if (playPauseBtnText) playPauseBtnText.textContent = 'Pause';
-
- // Start animation loop
- animationFrameId = requestAnimationFrame(animate);
- }
- // Pause animation
- function pauseAnimation() {
- isPlaying = false;
-
- // Update UI
- const playPauseBtn = document.getElementById('playPauseBtn');
- const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
- const playPauseBtnText = document.getElementById('playPauseBtnText');
-
- if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'play_arrow';
- if (playPauseBtnText) playPauseBtnText.textContent = 'Play';
-
- // Cancel animation frame
- if (animationFrameId) {
- cancelAnimationFrame(animationFrameId);
- animationFrameId = null;
- }
- }
- // Animation loop
- function animate(timestamp) {
- if (!isPlaying) return;
-
- const deltaTime = timestamp - lastTimestamp;
- const progressIncrement = (deltaTime / 1000) * animationSpeed * 2.0; // Much faster base speed
-
- currentProgress = Math.min(100, currentProgress + progressIncrement);
-
- // Update UI
- updateProgressUI();
-
- // Draw frame
- const canvas = document.getElementById('animatedPreviewCanvas');
- if (canvas) {
- drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
- }
-
- // Continue animation
- if (currentProgress < 100) {
- lastTimestamp = timestamp;
- animationFrameId = requestAnimationFrame(animate);
- } else {
- // Animation complete
- pauseAnimation();
- }
- }
- // Reset animation
- function resetAnimation() {
- pauseAnimation();
- currentProgress = 0;
- updateProgressUI();
-
- const canvas = document.getElementById('animatedPreviewCanvas');
- drawAnimatedPreview(canvas.getContext('2d'), 0);
- }
- // Update progress UI
- function updateProgressUI() {
- const progressSlider = document.getElementById('progressSlider');
- const progressValue = document.getElementById('progressValue');
-
- progressSlider.value = currentProgress;
- progressValue.textContent = `${currentProgress.toFixed(1)}%`;
- }
- // Close animated preview
- function closeAnimatedPreview() {
- pauseAnimation();
-
- const modal = document.getElementById('animatedPreviewModal');
- modal.classList.add('hidden');
-
- // Clear data
- animatedPreviewData = null;
- currentProgress = 0;
- animationSpeed = 1;
-
- // Reset UI
- const speedSlider = document.getElementById('speedSlider');
- const speedValue = document.getElementById('speedValue');
- const progressSlider = document.getElementById('progressSlider');
- const progressValue = document.getElementById('progressValue');
-
- speedSlider.value = 1;
- speedValue.textContent = '1x';
- progressSlider.value = 0;
- progressValue.textContent = '0%';
- }
|