Files
easystream/app_scripts/upload_progress_widget.js
Krystie 092a8bc7ce refactor: organize codebase and remove redundant files
- Removed all backup/duplicate files
- Removed test files from root
- Consolidated documentation to /docs/
- Moved scripts to /scripts/
- Renamed f_* directories (removed prefix)
- Organized icons and assets
- Removed unused vendor directories
- Cleaned up redundant config files
2026-03-30 16:34:45 -07:00

441 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<div class="upload-widget-header">
<h3>Uploads</h3>
<button class="collapse-btn" aria-label="Collapse"></button>
</div>
<div class="upload-widget-body">
<div class="upload-list"></div>
</div>
`;
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 = `
<div class="upload-header">
<div class="upload-filename" title="${upload.filename}">${upload.filename}</div>
${upload.status === 'uploading' || upload.status === 'processing' ?
`<button class="upload-cancel" data-upload-id="${uploadId}" aria-label="Cancel">×</button>` :
''}
</div>
<div class="upload-status">${statusText}</div>
<div class="upload-progress-bar">
<div class="upload-progress-fill ${progressClass}" style="width: ${upload.percent}%"></div>
</div>
<div class="upload-percent">${Math.round(upload.percent)}%</div>
${upload.errorMessage ? `<div class="upload-error">${upload.errorMessage}</div>` : ''}
`;
// 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;