/** * EasyStream Upload Progress Widget * * Displays real-time upload progress for files being uploaded * Features: * - Multiple simultaneous uploads * - Real-time progress bars * - Processing status indicators * - Cancel uploads * - Auto-hide when complete */ class UploadProgressWidget { constructor(options = {}) { this.options = { apiUrl: options.apiUrl || '/api/upload/progress.php', pollInterval: options.pollInterval || 1000, // Poll every second autoHide: options.autoHide !== false, hideDelay: options.hideDelay || 3000, ...options }; this.uploads = new Map(); this.widget = null; this.pollTimer = null; this.init(); } init() { this.createWidget(); this.loadExistingUploads(); } createWidget() { const widget = document.createElement('div'); widget.className = 'upload-progress-widget'; widget.innerHTML = `

Uploads

`; document.body.appendChild(widget); this.widget = widget; // Collapse functionality widget.querySelector('.collapse-btn').addEventListener('click', () => { widget.classList.toggle('collapsed'); widget.querySelector('.collapse-btn').textContent = widget.classList.contains('collapsed') ? '+' : '−'; }); this.injectStyles(); } injectStyles() { const style = document.createElement('style'); style.textContent = ` .upload-progress-widget { position: fixed; bottom: 20px; right: 20px; width: 380px; max-height: 500px; background: white; border-radius: 12px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); z-index: 9999; display: none; flex-direction: column; overflow: hidden; } .upload-progress-widget.visible { display: flex; } .upload-progress-widget.collapsed .upload-widget-body { display: none; } .upload-widget-header { padding: 16px 20px; background: #f9f9f9; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } .upload-widget-header h3 { margin: 0; font-size: 16px; font-weight: 600; } .collapse-btn { background: none; border: none; font-size: 20px; cursor: pointer; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } .upload-widget-body { overflow-y: auto; max-height: 440px; } .upload-list { padding: 12px; } .upload-item { padding: 12px; margin-bottom: 12px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0; } .upload-item.completed { background: #e8f5e9; border-color: #4caf50; } .upload-item.failed { background: #ffebee; border-color: #f44336; } .upload-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; } .upload-filename { font-weight: 500; font-size: 14px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .upload-cancel { background: none; border: none; color: #f44336; cursor: pointer; padding: 0 4px; font-size: 18px; } .upload-status { font-size: 12px; color: #666; margin-bottom: 8px; } .upload-progress-bar { height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; margin-bottom: 4px; } .upload-progress-fill { height: 100%; background: linear-gradient(90deg, #065fd4, #1e88e5); transition: width 0.3s ease; position: relative; } .upload-progress-fill.processing { background: linear-gradient(90deg, #f57c00, #ff9800); animation: pulse 1.5s ease-in-out infinite; } .upload-progress-fill.completed { background: #4caf50; } .upload-progress-fill.failed { background: #f44336; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } .upload-percent { font-size: 12px; color: #666; text-align: right; } .upload-error { font-size: 12px; color: #f44336; margin-top: 4px; } @media (max-width: 480px) { .upload-progress-widget { width: calc(100% - 40px); right: 20px; left: 20px; } } @media (prefers-color-scheme: dark) { .upload-progress-widget { background: #212121; } .upload-widget-header { background: #2a2a2a; border-bottom-color: #303030; } .upload-item { background: #2a2a2a; border-color: #303030; } } `; document.head.appendChild(style); } addUpload(uploadId, filename, fileType) { this.uploads.set(uploadId, { id: uploadId, filename: filename, fileType: fileType, status: 'uploading', percent: 0 }); this.renderUpload(uploadId); this.show(); this.startPolling(); } renderUpload(uploadId) { const upload = this.uploads.get(uploadId); if (!upload) return; const list = this.widget.querySelector('.upload-list'); let item = list.querySelector(`[data-upload-id="${uploadId}"]`); if (!item) { item = document.createElement('div'); item.className = 'upload-item'; item.dataset.uploadId = uploadId; list.appendChild(item); } const statusText = this.getStatusText(upload.status, upload.processingStep); const progressClass = upload.status === 'processing' || upload.status === 'encoding' ? 'processing' : upload.status === 'completed' ? 'completed' : upload.status === 'failed' ? 'failed' : ''; item.className = `upload-item ${upload.status}`; item.innerHTML = `
${upload.filename}
${upload.status === 'uploading' || upload.status === 'processing' ? `` : ''}
${statusText}
${Math.round(upload.percent)}%
${upload.errorMessage ? `
${upload.errorMessage}
` : ''} `; // Add cancel handler const cancelBtn = item.querySelector('.upload-cancel'); if (cancelBtn) { cancelBtn.addEventListener('click', () => this.cancelUpload(uploadId)); } } getStatusText(status, processingStep) { const statusMap = { 'uploading': 'Uploading...', 'processing': processingStep || 'Processing...', 'encoding': processingStep || 'Encoding video...', 'completed': 'Upload complete!', 'failed': 'Upload failed', 'cancelled': 'Cancelled' }; return statusMap[status] || status; } async updateStatus(uploadId) { try { const response = await fetch(`${this.options.apiUrl}?action=get_status&upload_id=${uploadId}`); const data = await response.json(); if (data.error) { console.error('Error fetching upload status:', data.error); return; } const upload = this.uploads.get(uploadId); if (upload) { upload.status = data.status; upload.percent = data.upload_percent; upload.processingStep = data.processing_step; upload.errorMessage = data.error_message; upload.fileKey = data.file_key; this.renderUpload(uploadId); // Auto-hide completed uploads if (data.status === 'completed' && this.options.autoHide) { setTimeout(() => { this.removeUpload(uploadId); }, this.options.hideDelay); } // Stop polling if all uploads are done if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') { const hasActive = Array.from(this.uploads.values()).some(u => u.status === 'uploading' || u.status === 'processing' || u.status === 'encoding' ); if (!hasActive) { this.stopPolling(); } } } } catch (error) { console.error('Error updating upload status:', error); } } async loadExistingUploads() { try { const response = await fetch(`${this.options.apiUrl}?action=get_all`); const data = await response.json(); if (data.uploads && data.uploads.length > 0) { data.uploads.forEach(upload => { this.uploads.set(upload.upload_id, { id: upload.upload_id, filename: upload.filename, fileType: upload.file_type, status: upload.status, percent: upload.upload_percent, processingStep: upload.processing_step }); this.renderUpload(upload.upload_id); }); this.show(); this.startPolling(); } } catch (error) { console.error('Error loading existing uploads:', error); } } async cancelUpload(uploadId) { try { const response = await fetch(`${this.options.apiUrl}?action=cancel&upload_id=${uploadId}`); const data = await response.json(); if (data.success) { this.removeUpload(uploadId); } } catch (error) { console.error('Error cancelling upload:', error); } } removeUpload(uploadId) { this.uploads.delete(uploadId); const item = this.widget.querySelector(`[data-upload-id="${uploadId}"]`); if (item) { item.remove(); } if (this.uploads.size === 0) { this.hide(); } } startPolling() { if (this.pollTimer) return; this.pollTimer = setInterval(() => { this.uploads.forEach((upload, id) => { if (upload.status === 'uploading' || upload.status === 'processing' || upload.status === 'encoding') { this.updateStatus(id); } }); }, this.options.pollInterval); } stopPolling() { if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; } } show() { this.widget.classList.add('visible'); } hide() { this.widget.classList.remove('visible'); this.stopPolling(); } } // Export for use in other scripts window.UploadProgressWidget = UploadProgressWidget;