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
This commit is contained in:
440
f_scripts/upload_progress_widget.js
Normal file
440
f_scripts/upload_progress_widget.js
Normal file
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user