import { translate } from '../utils/i18nHelpers.js'; import { formatFileSize } from '../utils/formatters.js'; // Loading management export class LoadingManager { constructor() { if (LoadingManager.instance) { return LoadingManager.instance; } // Delay DOM creation until first use to ensure i18n is ready this._initialized = false; this.overlay = null; this.loadingContent = null; this.progressBar = null; this.statusText = null; this.cancelButton = null; this.onCancelCallback = null; this.detailsContainer = null; LoadingManager.instance = this; } _ensureInitialized() { if (this._initialized) return; this.overlay = document.getElementById('loading-overlay'); if (!this.overlay) { this.overlay = document.createElement('div'); this.overlay.id = 'loading-overlay'; this.overlay.style.display = 'none'; document.body.appendChild(this.overlay); } this.loadingContent = this.overlay.querySelector('.loading-content'); if (!this.loadingContent) { this.loadingContent = document.createElement('div'); this.loadingContent.className = 'loading-content'; this.overlay.appendChild(this.loadingContent); } this.progressBar = this.loadingContent.querySelector('.progress-bar'); if (!this.progressBar) { this.progressBar = document.createElement('div'); this.progressBar.className = 'progress-bar'; this.progressBar.setAttribute('role', 'progressbar'); this.progressBar.setAttribute('aria-valuemin', '0'); this.progressBar.setAttribute('aria-valuemax', '100'); this.progressBar.setAttribute('aria-valuenow', '0'); this.progressBar.style.width = '0%'; this.loadingContent.appendChild(this.progressBar); } this.statusText = this.loadingContent.querySelector('.loading-status'); if (!this.statusText) { this.statusText = document.createElement('div'); this.statusText.className = 'loading-status'; this.loadingContent.appendChild(this.statusText); } this.cancelButton = this.loadingContent.querySelector('.loading-cancel'); if (!this.cancelButton) { this.cancelButton = document.createElement('button'); this.cancelButton.className = 'loading-cancel secondary-btn'; this.cancelButton.style.display = 'none'; this.cancelButton.style.margin = 'var(--space-2) auto 0'; this.cancelButton.textContent = translate('common.actions.cancel', {}, 'Cancel'); this.loadingContent.appendChild(this.cancelButton); } this.cancelButton.onclick = () => { if (this.onCancelCallback) { this.onCancelCallback(); this.cancelButton.disabled = true; this.cancelButton.textContent = translate('common.status.loading', {}, 'Loading...'); } }; this._initialized = true; } show(message = 'Loading...', progress = 0) { this._ensureInitialized(); this.overlay.style.display = 'flex'; this.setProgress(progress); this.setStatus(message); // Remove any existing details container this.removeDetailsContainer(); } hide() { if (!this._initialized) return; this.overlay.style.display = 'none'; this.reset(); this.removeDetailsContainer(); } setProgress(percent) { if (!this._initialized) return; this.progressBar.style.width = `${percent}%`; this.progressBar.setAttribute('aria-valuenow', percent); } setStatus(message) { if (!this._initialized) return; this.statusText.textContent = message; } reset() { if (!this._initialized) return; this.setProgress(0); this.setStatus(''); this.removeDetailsContainer(); this.hideCancelButton(); this.progressBar.style.display = 'block'; } showCancelButton(onCancel) { this._ensureInitialized(); if (this.cancelButton) { this.onCancelCallback = onCancel; this.cancelButton.style.display = 'flex'; this.cancelButton.disabled = false; this.cancelButton.textContent = translate('common.actions.cancel', {}, 'Cancel'); } } hideCancelButton() { if (!this._initialized) return; if (this.cancelButton) { this.cancelButton.style.display = 'none'; this.onCancelCallback = null; } } // Create a details container for enhanced progress display createDetailsContainer() { this._ensureInitialized(); // Remove existing container if any this.removeDetailsContainer(); // Create new container this.detailsContainer = document.createElement('div'); this.detailsContainer.className = 'progress-details-container'; // Insert after the main progress bar if (this.loadingContent) { this.loadingContent.appendChild(this.detailsContainer); } return this.detailsContainer; } // Remove details container removeDetailsContainer() { if (this.detailsContainer) { this.detailsContainer.remove(); this.detailsContainer = null; } } // Show enhanced progress for downloads showDownloadProgress(totalItems = 1) { this.show(translate('modals.download.status.preparing', {}, 'Preparing download...'), 0); this.progressBar.style.display = 'none'; // Create details container const detailsContainer = this.createDetailsContainer(); // Create current item progress const currentItemContainer = document.createElement('div'); currentItemContainer.className = 'current-item-progress'; const currentItemLabel = document.createElement('div'); currentItemLabel.className = 'current-item-label'; currentItemLabel.textContent = translate('modals.download.progress.currentFile', {}, 'Current file:'); const currentItemBar = document.createElement('div'); currentItemBar.className = 'current-item-bar-container'; const currentItemProgress = document.createElement('div'); currentItemProgress.className = 'current-item-bar'; currentItemProgress.style.width = '0%'; const currentItemPercent = document.createElement('span'); currentItemPercent.className = 'current-item-percent'; currentItemPercent.textContent = '0%'; currentItemBar.appendChild(currentItemProgress); currentItemContainer.appendChild(currentItemLabel); currentItemContainer.appendChild(currentItemBar); currentItemContainer.appendChild(currentItemPercent); // Create overall progress elements if multiple items let overallLabel = null; if (totalItems > 1) { overallLabel = document.createElement('div'); overallLabel.className = 'overall-progress-label'; overallLabel.textContent = `Overall progress (0/${totalItems} complete):`; detailsContainer.appendChild(overallLabel); } // Add current item progress to container detailsContainer.appendChild(currentItemContainer); // Create transfer stats container const transferStats = document.createElement('div'); transferStats.className = 'download-transfer-stats'; const bytesDetail = document.createElement('div'); bytesDetail.className = 'download-transfer-bytes'; bytesDetail.textContent = translate( 'modals.download.progress.transferredUnknown', {}, 'Transferred: --' ); const speedDetail = document.createElement('div'); speedDetail.className = 'download-transfer-speed'; speedDetail.textContent = translate( 'modals.download.progress.speed', { speed: '--' }, 'Speed: --' ); transferStats.appendChild(bytesDetail); transferStats.appendChild(speedDetail); detailsContainer.appendChild(transferStats); const formatMetricSize = (value) => { if (value === undefined || value === null || isNaN(value)) { return '--'; } if (value < 1) { return '0 B'; } return formatFileSize(value); }; const updateTransferStats = (metrics = {}) => { const { bytesDownloaded, totalBytes, bytesPerSecond } = metrics; if (bytesDetail) { const formattedDownloaded = formatMetricSize(bytesDownloaded); const formattedTotal = formatMetricSize(totalBytes); if (formattedDownloaded === '--' && formattedTotal === '--') { bytesDetail.textContent = translate( 'modals.download.progress.transferredUnknown', {}, 'Transferred: --' ); } else if (formattedTotal === '--') { bytesDetail.textContent = translate( 'modals.download.progress.transferredSimple', { downloaded: formattedDownloaded }, `Transferred: ${formattedDownloaded}` ); } else { bytesDetail.textContent = translate( 'modals.download.progress.transferred', { downloaded: formattedDownloaded, total: formattedTotal }, `Transferred: ${formattedDownloaded} / ${formattedTotal}` ); } } if (speedDetail) { const formattedSpeed = formatMetricSize(bytesPerSecond); const displaySpeed = formattedSpeed === '--' ? '--' : `${formattedSpeed}/s`; speedDetail.textContent = translate( 'modals.download.progress.speed', { speed: displaySpeed }, `Speed: ${displaySpeed}` ); } }; // Initialize transfer stats with empty data updateTransferStats(); // Return update function return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => { // Update current item progress currentItemProgress.style.width = `${currentProgress}%`; currentItemPercent.textContent = `${Math.floor(currentProgress)}%`; // Update current item label if name provided if (currentName) { currentItemLabel.textContent = translate( 'modals.download.progress.downloading', { name: currentName }, `Downloading: ${currentName}` ); } // Update overall label if multiple items if (totalItems > 1 && overallLabel) { overallLabel.textContent = `Overall progress (${currentIndex}/${totalItems} complete):`; // Calculate and update overall progress const overallProgress = Math.floor((currentIndex + currentProgress / 100) / totalItems * 100); this.setProgress(overallProgress); } else { // Single item, just update main progress this.setProgress(currentProgress); } updateTransferStats(metrics); }; } async showWithProgress(callback, options = {}) { const { initialMessage = 'Processing...', completionMessage = 'Complete' } = options; try { this.show(initialMessage); await callback(this); this.setProgress(100); this.setStatus(completionMessage); await new Promise(resolve => setTimeout(resolve, 500)); } finally { this.hide(); } } // Enhanced progress display without callback pattern showEnhancedProgress(message = 'Processing...') { this.show(message, 0); // Return update functions return { updateProgress: (percent, currentItem = '', statusMessage = '') => { this.setProgress(percent); if (statusMessage) { this.setStatus(statusMessage); } }, showCancelButton: (onCancel) => { this.showCancelButton(onCancel); }, complete: async (completionMessage = 'Complete') => { this.setProgress(100); this.setStatus(completionMessage); await new Promise(resolve => setTimeout(resolve, 500)); this.hide(); } }; } showSimpleLoading(message = 'Loading...') { this._ensureInitialized(); this.overlay.style.display = 'flex'; this.progressBar.style.display = 'none'; this.setStatus(message); } restoreProgressBar() { if (!this._initialized) return; this.progressBar.style.display = 'block'; } }