// Universal Media Downloader - Enhanced JavaScript // Professional media downloader with API integration class UniversalMediaDownloader { constructor() { this.apiBaseUrl = '/api'; this.currentFormats = []; this.currentMediaInfo = null; this.isProcessing = false; this.downloadQueue = []; this.settings = this.loadSettings(); this.activeDownloads = new Map(); this.progressIntervals = new Map(); this.initializeApp(); } initializeApp() { this.setupEventListeners(); this.setupKeyboardNavigation(); this.checkApiHealth(); this.setupServiceWorker(); this.loadDownloadHistory(); this.announceAppReady(); } // Settings Management loadSettings() { const defaultSettings = { autoUpdate: true, privacyMode: false, defaultQuality: 'best', theme: 'dark' }; try { const saved = localStorage.getItem('downloader-settings'); return { ...defaultSettings, ...(saved ? JSON.parse(saved) : {}) }; } catch { return defaultSettings; } } saveSettings() { try { localStorage.setItem('downloader-settings', JSON.stringify(this.settings)); this.announceToScreenReader('Settings saved successfully'); } catch (error) { console.error('Failed to save settings:', error); } } // API Communication async checkApiHealth() { try { const response = await fetch(`${this.apiBaseUrl}/health`); const data = await response.json(); if (data.status === 'healthy') { this.updateStatus('API connected and ready', 'success'); this.updateApiInfo(data); } else { throw new Error('API not healthy'); } } catch (error) { console.error('API health check failed:', error); this.showError('Unable to connect to download service. Please refresh the page.'); } } async getFormats(url) { try { const response = await fetch(`${this.apiBaseUrl}/formats`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }) }); const data = await response.json(); if (!response.ok) { // Check if we have fallback download options if (data.fallback && data.download_options) { return { success: false, fallback: true, download_options: data.download_options, basic_info: data.basic_info, message: data.message, instruction: data.instruction }; } throw new Error(data.message || data.error || 'Failed to get formats'); } return data; } catch (error) { console.error('Get formats error:', error); throw error; } } async startDownload(url, formatId) { try { const downloadId = `dl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const response = await fetch(`${this.apiBaseUrl}/download`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url, format_id: formatId, download_id: downloadId }) }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || data.error || 'Failed to start download'); } // Start monitoring progress this.startProgressMonitoring(downloadId); return { ...data, downloadId }; } catch (error) { console.error('Start download error:', error); throw error; } } async getProgress(downloadId) { try { const response = await fetch(`${this.apiBaseUrl}/progress/${downloadId}`); const data = await response.json(); return data; } catch (error) { console.error('Get progress error:', error); return null; } } async getSupportedPlatforms() { try { const response = await fetch(`${this.apiBaseUrl}/supported-platforms`); const data = await response.json(); return data; } catch (error) { console.error('Get platforms error:', error); return null; } } async updateYtdlp() { try { this.updateStatus('Checking for yt-dlp updates...', 'info'); const response = await fetch(`${this.apiBaseUrl}/update`, { method: 'POST' }); const data = await response.json(); if (data.success) { this.updateStatus(`yt-dlp updated to version: ${data.version}`, 'success'); this.announceToScreenReader(`yt-dlp updated to version ${data.version}`); } else { this.updateStatus(data.message || 'Update check completed', 'info'); } return data; } catch (error) { console.error('Update yt-dlp error:', error); this.showError('Failed to update yt-dlp'); throw error; } } // Progress Monitoring startProgressMonitoring(downloadId) { if (this.progressIntervals.has(downloadId)) { clearInterval(this.progressIntervals.get(downloadId)); } const interval = setInterval(async () => { const progress = await this.getProgress(downloadId); if (progress && progress.success) { this.updateDownloadProgress(progress); if (progress.progress.status === 'finished' || progress.progress.status === 'error') { clearInterval(interval); this.progressIntervals.delete(downloadId); if (progress.progress.status === 'finished') { this.onDownloadComplete(downloadId, progress.progress); } else { this.onDownloadError(downloadId, progress.progress); } } } }, 1000); this.progressIntervals.set(downloadId, interval); } updateDownloadProgress(progressData) { const { downloadId, progress } = progressData; // Update queue item const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); if (queueItem) { this.updateQueueItem(queueItem, progress); } // Update global progress if (progress.status === 'downloading') { this.showProgress(progress.percentage, progress.speed, progress.eta); } } // UI Updates updateStatus(message, type = 'info') { const statusContainer = document.getElementById('status-container'); const statusIcon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'; statusContainer.innerHTML = `
${statusIcon} ${message}
`; this.announceToScreenReader(message); } showError(message) { const errorSection = document.getElementById('error-section'); const errorContainer = document.getElementById('error-messages'); errorContainer.innerHTML = `
${message}
`; errorSection.style.display = 'block'; this.announceToScreenReader(`Error: ${message}`); } hideError() { const errorSection = document.getElementById('error-section'); errorSection.style.display = 'none'; } showProgress(percentage, speed = null, eta = null) { const progressSection = document.getElementById('progress-section'); const progressBar = document.getElementById('progress-bar'); const progressPercentage = document.getElementById('progress-percentage'); const progressSpeed = document.getElementById('progress-speed'); const progressTitle = document.getElementById('progress-title'); progressSection.style.display = 'block'; progressBar.value = percentage; progressPercentage.textContent = `${Math.round(percentage)}%`; if (speed) { progressSpeed.textContent = this.formatSpeed(speed); } if (eta) { progressTitle.textContent = `Downloading... ETA: ${this.formatTime(eta)}`; } } hideProgress() { const progressSection = document.getElementById('progress-section'); progressSection.style.display = 'none'; } displayMediaInfo(info) { const section = document.getElementById('media-info-section'); const title = document.getElementById('media-title'); const uploader = document.getElementById('media-uploader'); const platform = document.getElementById('media-platform'); const duration = document.getElementById('media-duration'); const views = document.getElementById('media-views'); const likes = document.getElementById('media-likes'); const thumb = document.getElementById('media-thumb'); const placeholder = document.getElementById('thumb-placeholder'); // Update content title.textContent = info.title || 'Unknown Title'; uploader.textContent = info.uploader || 'Unknown Uploader'; platform.textContent = info.platform || 'Unknown'; platform.className = `platform-badge platform-${(info.platform || '').toLowerCase()}`; duration.textContent = info.duration ? this.formatTime(info.duration) : ''; views.textContent = info.view_count ? `${this.formatNumber(info.view_count)} views` : ''; likes.textContent = info.like_count ? `${this.formatNumber(info.like_count)} likes` : ''; // Handle thumbnail if (info.thumbnail) { thumb.src = info.thumbnail; thumb.alt = `Thumbnail for ${info.title}`; thumb.style.display = 'block'; placeholder.style.display = 'none'; } else { thumb.style.display = 'none'; placeholder.style.display = 'flex'; } section.style.display = 'block'; } displayFormats(formats) { const section = document.getElementById('formats-section'); const formatList = document.getElementById('format-list'); // Clear existing formats formatList.innerHTML = ''; // Sort formats by quality and type const sortedFormats = this.sortFormats(formats); sortedFormats.forEach((format, index) => { const formatElement = this.createFormatElement(format, index); formatList.appendChild(formatElement); }); section.style.display = 'block'; this.currentFormats = sortedFormats; } createFormatElement(format, index) { const element = document.createElement('div'); element.className = 'format-item'; element.setAttribute('role', 'listitem'); element.setAttribute('data-format-id', format.id); element.setAttribute('data-format-type', format.type); element.setAttribute('tabindex', '0'); const fileSize = format.filesize ? this.formatFileSize(format.filesize) : 'Unknown size'; const quality = format.width && format.height ? `${format.height}p` : format.format_note || 'Unknown'; const codec = format.vcodec !== 'none' ? format.vcodec.split('.')[0] : format.acodec.split('.')[0]; element.innerHTML = `
${this.getFormatTitle(format)}
${format.ext.toUpperCase()} • ${quality} • ${codec} • ${fileSize}
`; // Add event listeners element.addEventListener('click', (e) => { if (!e.target.closest('.download-btn')) { this.selectFormat(format); } }); element.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.selectFormat(format); } }); const downloadBtn = element.querySelector('.download-btn'); downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); this.handleDownload(format, downloadBtn); }); return element; } createQueueItem(progress) { const element = document.createElement('div'); element.className = 'queue-item'; element.setAttribute('data-download-id', progress.download_id); const fileName = progress.filename ? progress.filename.split('/').pop() : 'Downloading...'; element.innerHTML = `
${fileName}
${progress.url || 'Downloading...'}
Starting...
`; return element; } updateQueueItem(element, progress) { const progressBar = element.querySelector('.progress-bar'); const statusText = element.querySelector('.queue-status'); if (progress.status === 'downloading') { progressBar.value = progress.percentage || 0; statusText.textContent = `${Math.round(progress.percentage || 0)}%`; } else if (progress.status === 'finished') { element.classList.add('completed'); statusText.textContent = 'Complete'; progressBar.value = 100; } else if (progress.status === 'error') { element.classList.add('error'); statusText.textContent = 'Error'; } } // Event Handlers setupEventListeners() { // Form submission const form = document.getElementById('download-form'); form.addEventListener('submit', (e) => this.handleFormSubmit(e)); // URL input const urlInput = document.getElementById('url-input'); urlInput.addEventListener('input', () => this.validateURL()); urlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.handleFormSubmit(e); } }); // Format filters const filterBtns = document.querySelectorAll('.filter-btn'); filterBtns.forEach(btn => { btn.addEventListener('click', (e) => this.handleFilterClick(e)); }); // Settings modal const settingsBtn = document.getElementById('settings-btn'); const settingsModal = document.getElementById('settings-modal'); const modalClose = document.querySelector('.modal-close'); const saveSettingsBtn = document.getElementById('save-settings'); settingsBtn.addEventListener('click', () => this.openSettings()); modalClose.addEventListener('click', () => this.closeSettings()); settingsModal.addEventListener('click', (e) => { if (e.target === settingsModal) this.closeSettings(); }); saveSettingsBtn.addEventListener('click', () => this.saveSettingsAndClose()); // Update button const updateBtn = document.getElementById('update-btn'); updateBtn.addEventListener('click', () => this.handleUpdateYtdlp()); // Clear queue button const clearQueueBtn = document.getElementById('clear-queue-btn'); clearQueueBtn.addEventListener('click', () => this.clearQueue()); // Bottom navigation const navItems = document.querySelectorAll('.nav-item'); navItems.forEach(item => { item.addEventListener('click', (e) => this.handleNavClick(e)); }); // Settings inputs this.setupSettingsInputs(); } setupSettingsInputs() { const autoUpdate = document.getElementById('auto-update'); const privacyMode = document.getElementById('privacy-mode'); const defaultQuality = document.getElementById('default-quality'); autoUpdate.checked = this.settings.autoUpdate; privacyMode.checked = this.settings.privacyMode; defaultQuality.value = this.settings.defaultQuality; } async handleFormSubmit(event) { event.preventDefault(); if (this.isProcessing) { return; } this.hideError(); this.hideProgress(); const url = document.getElementById('url-input').value.trim(); if (!this.isValidURL(url)) { this.showError('Please enter a valid URL. Make sure it starts with http:// or https://'); return; } this.setLoadingState(true); this.updateStatus('Analyzing URL and fetching available formats...', 'info'); try { const result = await this.getFormats(url); // Handle fallback case with download options if (result.fallback && result.download_options) { this.displayDownloadOptions(result); this.updateStatus(result.message || 'Format extraction failed, but direct download options are available.', 'warning'); return; } if (result.success) { this.currentMediaInfo = { title: result.title, uploader: result.uploader, platform: result.platform, duration: result.duration, thumbnail: result.thumbnail, view_count: result.view_count, like_count: result.like_count }; this.displayMediaInfo(this.currentMediaInfo); this.displayFormats(result.formats); this.updateStatus(`Found ${result.formats.length} available formats for ${result.platform}. Select your preferred option below.`, 'success'); // Add to queue this.addToQueue(result); } else { throw new Error(result.message || result.error || 'Failed to analyze URL'); } } catch (error) { console.error('Form submission error:', error); this.showError(error.message || 'Failed to analyze URL. Please check if the URL is valid and try again.'); } finally { this.setLoadingState(false); } } async handleDownload(format, button) { if (this.isProcessing) { return; } try { this.setDownloadButtonState(button, 'downloading'); this.updateStatus(`Starting download: ${this.getFormatTitle(format)}`, 'info'); const result = await this.startDownload( document.getElementById('url-input').value.trim(), format.id ); // Add to active downloads this.activeDownloads.set(result.downloadId, { format, startTime: Date.now() }); // Add to queue display const queueList = document.getElementById('queue-list'); const queueItem = this.createQueueItem({ download_id: result.downloadId, filename: this.getFormatTitle(format), url: document.getElementById('url-input').value.trim() }); queueList.appendChild(queueItem); this.updateStatus(`Download started: ${this.getFormatTitle(format)}`, 'success'); this.announceToScreenReader(`Download started: ${this.getFormatTitle(format)}`); } catch (error) { console.error('Download error:', error); this.setDownloadButtonState(button, 'error'); this.showError(`Download failed: ${error.message}`); } } async handleUpdateYtdlp() { const updateBtn = document.getElementById('update-btn'); updateBtn.disabled = true; updateBtn.style.opacity = '0.6'; try { await this.updateYtdlp(); } catch (error) { console.error('Update error:', error); } finally { updateBtn.disabled = false; updateBtn.style.opacity = '1'; } } handleFilterClick(event) { const filter = event.target.dataset.filter; const buttons = document.querySelectorAll('.filter-btn'); // Update active filter buttons.forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); // Filter formats const formatItems = document.querySelectorAll('.format-item'); formatItems.forEach(item => { const formatType = item.dataset.formatType; if (filter === 'all' || formatType === filter) { item.style.display = 'flex'; } else { item.style.display = 'none'; } }); } handleNavClick(event) { const tab = event.currentTarget.dataset.tab; const items = document.querySelectorAll('.nav-item'); items.forEach(item => item.classList.remove('active')); event.currentTarget.classList.add('active'); // Show/hide relevant sections based on tab this.showTabContent(tab); } showTabContent(tab) { // This would show/hide different content areas // For now, we'll just announce the tab change this.announceToScreenReader(`Switched to ${tab} tab`); } selectFormat(format) { // Highlight selected format const formatItems = document.querySelectorAll('.format-item'); formatItems.forEach(item => item.classList.remove('selected')); const selectedItem = document.querySelector(`[data-format-id="${format.id}"]`); if (selectedItem) { selectedItem.classList.add('selected'); this.announceToScreenReader(`Selected ${this.getFormatTitle(format)}`); } } // Utility Methods isValidURL(string) { try { new URL(string); return true; } catch (_) { return false; } } validateURL() { const urlInput = document.getElementById('url-input'); const url = urlInput.value.trim(); if (url === '') { urlInput.setCustomValidity(''); return true; } if (!this.isValidURL(url)) { urlInput.setCustomValidity('Please enter a valid URL'); urlInput.reportValidity(); return false; } urlInput.setCustomValidity(''); return true; } setLoadingState(isLoading) { this.isProcessing = isLoading; const button = document.getElementById('analyze-btn'); const urlInput = document.getElementById('url-input'); if (isLoading) { button.classList.add('loading'); button.disabled = true; urlInput.disabled = true; } else { button.classList.remove('loading'); button.disabled = false; urlInput.disabled = false; urlInput.focus(); } } setDownloadButtonState(button, state) { button.disabled = state === 'downloading'; button.classList.remove('downloading', 'error'); if (state === 'downloading') { button.classList.add('downloading'); button.innerHTML = 'Downloading...'; } else if (state === 'error') { button.classList.add('error'); button.innerHTML = 'Error'; } else { button.innerHTML = 'Download'; } } addToQueue(result) { // Add media to download queue for easy access this.downloadQueue.push({ url: document.getElementById('url-input').value.trim(), title: result.title, platform: result.platform, formats: result.formats, timestamp: Date.now() }); } clearQueue() { this.downloadQueue = []; const queueList = document.getElementById('queue-list'); queueList.innerHTML = ''; this.announceToScreenReader('Download queue cleared'); } onDownloadComplete(downloadId, progress) { this.setDownloadCompleted(downloadId); this.updateStatus('Download completed successfully!', 'success'); this.announceToScreenReader('Download completed successfully'); this.hideProgress(); } onDownloadError(downloadId, progress) { this.setDownloadError(downloadId); this.updateStatus('Download failed. Please try again.', 'error'); this.announceToScreenReader('Download failed'); this.hideProgress(); } setDownloadCompleted(downloadId) { const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); if (queueItem) { queueItem.classList.add('completed'); const statusText = queueItem.querySelector('.queue-status'); if (statusText) statusText.textContent = 'Complete'; } } setDownloadError(downloadId) { const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); if (queueItem) { queueItem.classList.add('error'); const statusText = queueItem.querySelector('.queue-status'); if (statusText) statusText.textContent = 'Error'; } } // Settings Modal openSettings() { const modal = document.getElementById('settings-modal'); modal.style.display = 'flex'; this.announceToScreenReader('Settings opened'); } closeSettings() { const modal = document.getElementById('settings-modal'); modal.style.display = 'none'; this.announceToScreenReader('Settings closed'); } saveSettingsAndClose() { const autoUpdate = document.getElementById('auto-update').checked; const privacyMode = document.getElementById('privacy-mode').checked; const defaultQuality = document.getElementById('default-quality').value; this.settings = { ...this.settings, autoUpdate, privacyMode, defaultQuality }; this.saveSettings(); this.closeSettings(); } // Formatting Utilities getFormatTitle(format) { const type = format.type === 'video' ? 'Video' : 'Audio'; const quality = format.height ? `${format.height}p` : format.format_note || 'Best'; return `${type} - ${quality}`; } sortFormats(formats) { // Sort by type (video first), then by quality return formats.sort((a, b) => { if (a.type !== b.type) { return a.type === 'video' ? -1 : 1; } if (a.height && b.height) { return b.height - a.height; } return 0; }); } formatFileSize(bytes) { if (!bytes) return 'Unknown'; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } formatSpeed(bytesPerSec) { if (!bytesPerSec) return ''; return this.formatFileSize(bytesPerSec) + '/s'; } formatTime(seconds) { if (!seconds) return ''; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } return `${minutes}:${secs.toString().padStart(2, '0')}`; } formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } updateApiInfo(data) { // Update any UI elements showing API info console.log('API Info:', data); } // Accessibility announceToScreenReader(message) { const announcement = document.createElement('div'); announcement.setAttribute('aria-live', 'polite'); announcement.setAttribute('aria-atomic', 'true'); announcement.className = 'sr-only'; announcement.textContent = message; document.body.appendChild(announcement); setTimeout(() => { document.body.removeChild(announcement); }, 1000); } setupKeyboardNavigation() { document.addEventListener('keydown', (e) => { // Global keyboard shortcuts if (e.ctrlKey || e.metaKey) { switch (e.key) { case 'u': e.preventDefault(); document.getElementById('url-input').focus(); break; case 's': e.preventDefault(); this.openSettings(); break; } } if (e.key === 'Escape') { this.closeSettings(); } }); } // Service Worker setupServiceWorker() { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker registered:', registration); }) .catch(error => { console.log('Service Worker registration failed:', error); }); } } loadDownloadHistory() { // Load download history from localStorage try { const history = localStorage.getItem('download-history'); if (history) { const parsed = JSON.parse(history); console.log('Loaded download history:', parsed.length, 'items'); } } catch (error) { console.error('Failed to load download history:', error); } } displayDownloadOptions(result) { const section = document.getElementById('formats-section'); const formatList = document.getElementById('format-list'); // Clear existing content formatList.innerHTML = ''; // Show basic information if available if (result.basic_info) { const info = result.basic_info; this.currentMediaInfo = { title: info.title, uploader: info.uploader, platform: info.platform, url: info.url }; this.displayMediaInfo(this.currentMediaInfo); } // Create download options section const optionsContainer = document.createElement('div'); optionsContainer.className = 'download-options-container'; const titleElement = document.createElement('h3'); titleElement.textContent = 'Direct File Downloads'; titleElement.className = 'download-options-title'; const instructionElement = document.createElement('p'); instructionElement.textContent = result.instruction || 'Click on any direct file link below to download immediately:'; instructionElement.className = 'download-instruction'; optionsContainer.appendChild(titleElement); optionsContainer.appendChild(instructionElement); // Create download links for direct files if (result.download_options && Array.isArray(result.download_options)) { result.download_options.forEach((option, index) => { const linkElement = document.createElement('a'); linkElement.href = option.url; linkElement.target = '_blank'; linkElement.rel = 'noopener noreferrer'; linkElement.className = 'download-link'; // Create icon based on type const icon = document.createElement('span'); icon.className = 'download-icon'; icon.innerHTML = this.getFileTypeIcon(option.type); // Create link text with details const textContainer = document.createElement('div'); textContainer.className = 'download-text-container'; const mainText = document.createElement('div'); mainText.className = 'download-text'; mainText.textContent = option.description || `${option.type} ${option.quality}`; const fileInfo = document.createElement('div'); fileInfo.className = 'download-file-info'; const fileDetails = []; if (option.ext && option.ext !== 'unknown') { fileDetails.push(option.ext.toUpperCase()); } if (option.filesize) { fileDetails.push(option.filesize); } if (option.format_id && option.format_id !== 'unknown') { fileDetails.push(`Format: ${option.format_id}`); } fileInfo.textContent = fileDetails.join(' • '); textContainer.appendChild(mainText); textContainer.appendChild(fileInfo); linkElement.appendChild(icon); linkElement.appendChild(textContainer); optionsContainer.appendChild(linkElement); }); } else { // Fallback for simple URL links result.download_options.forEach((url, index) => { const linkElement = document.createElement('a'); linkElement.href = url; linkElement.target = '_blank'; linkElement.rel = 'noopener noreferrer'; linkElement.className = 'download-link'; const icon = document.createElement('span'); icon.className = 'download-icon'; icon.innerHTML = this.getDownloadIcon(); const text = document.createElement('span'); text.className = 'download-text'; if (url.includes('youtube.com/watch')) { text.textContent = `YouTube Direct Link ${index + 1}`; } else if (url.includes('youtu.be/')) { text.textContent = `YouTube Short Link ${index + 1}`; } else { text.textContent = `Direct Link ${index + 1}`; } linkElement.appendChild(icon); linkElement.appendChild(text); optionsContainer.appendChild(linkElement); }); } formatList.appendChild(optionsContainer); section.style.display = 'block'; // Update current formats to empty since we don't have format-specific downloads this.currentFormats = []; } getDownloadIcon() { return ` `; } getFileTypeIcon(type) { const icons = { 'video': ` `, 'audio': ` `, 'image': ` `, 'thumbnail': ` `, 'direct': ` ` }; return icons[type] || icons['direct']; } announceAppReady() { setTimeout(() => { this.announceToScreenReader('Universal Media Downloader ready. Enter a URL to begin downloading.'); document.getElementById('url-input').focus(); }, 1000); } } // Initialize the application document.addEventListener('DOMContentLoaded', () => { window.downloader = new UniversalMediaDownloader(); }); // Global error handling window.addEventListener('error', (event) => { console.error('Global error:', event.error); if (window.downloader) { window.downloader.showError('An unexpected error occurred. Please refresh the page and try again.'); } }); window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise rejection:', event.reason); if (window.downloader) { window.downloader.showError('A background process failed. Please check your connection and try again.'); } });