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:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View 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;