Files
easystream-main/f_scripts/upload_progress_widget.js
SamiAhmed7777 0b7e2d0a5b feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
2025-10-21 00:39:45 -07:00

441 lines
13 KiB
JavaScript
Raw 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;