some changes but not implemented views for dashboard yes

This commit is contained in:
pika 2025-03-22 12:50:03 +01:00
parent acb3c7642a
commit eb93961967
5 changed files with 1044 additions and 135 deletions

401
app/static/css/browser.css Normal file
View file

@ -0,0 +1,401 @@
/* File Browser Styles */
.browser-container {
background: var(--card-bg);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-md);
padding: 1.5rem;
margin-bottom: 2rem;
}
.browser-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.browser-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.view-toggle {
display: flex;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
overflow: hidden;
margin-left: 0.5rem;
}
.view-btn {
background: var(--card-bg);
border: none;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: all 0.2s ease;
}
.view-btn:hover {
background: var(--bg-hover);
}
.view-btn.active {
background: var(--primary-color);
color: white;
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1.5rem;
padding: 0.75rem;
background: var(--bg-alt);
border-radius: var(--border-radius-sm);
}
.breadcrumb-item {
color: var(--text-color);
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius-sm);
transition: background 0.2s ease;
}
.breadcrumb-item:hover {
background: var(--bg-hover);
}
.breadcrumb-separator {
margin: 0 0.25rem;
color: var(--text-muted);
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-muted);
}
.loading-spinner i {
font-size: 2rem;
margin-bottom: 1rem;
}
.files-grid {
display: grid;
gap: 1rem;
}
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
.list-view {
grid-template-columns: 1fr;
}
.folder-item,
.file-item {
display: flex;
flex-direction: column;
text-decoration: none;
color: var(--text-color);
background: var(--bg-alt);
border-radius: var(--border-radius-sm);
transition: all 0.2s ease;
overflow: hidden;
position: relative;
}
.list-view .folder-item,
.list-view .file-item {
flex-direction: row;
align-items: center;
padding: 0.75rem;
}
.folder-item:hover,
.file-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.item-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
padding: 1.5rem 0;
background: var(--card-bg);
}
.list-view .item-icon {
font-size: 1.5rem;
padding: 0.5rem;
margin-right: 1rem;
background: none;
}
.folder-item .item-icon {
color: var(--folder-color, #f8d775);
}
.item-info {
padding: 0.75rem;
flex: 1;
}
.item-name {
font-weight: 500;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-details {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--text-muted);
}
.empty-folder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 1rem;
text-align: center;
color: var(--text-muted);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.5;
}
.empty-message h3 {
margin-bottom: 0.5rem;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow-y: auto;
animation: fadeIn 0.3s ease;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-lg);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: modalIn 0.3s ease;
}
.modal-lg {
max-width: 800px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-muted);
transition: color 0.2s ease;
}
.modal-close:hover {
color: var(--text-color);
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
/* File Preview Styles */
.image-preview,
.pdf-preview,
.video-preview {
display: flex;
justify-content: center;
align-items: center;
max-height: 60vh;
overflow: hidden;
border-radius: var(--border-radius-sm);
}
.image-preview img {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
}
.pdf-preview iframe,
.video-preview video {
width: 100%;
height: 60vh;
border: none;
}
.audio-preview {
display: flex;
justify-content: center;
padding: 2rem 0;
}
.text-preview {
max-height: 60vh;
overflow: auto;
background: var(--code-bg);
border-radius: var(--border-radius-sm);
}
.text-preview pre {
margin: 0;
padding: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
font-family: monospace;
}
.file-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.detail-item {
display: flex;
align-items: baseline;
}
.detail-label {
font-weight: 500;
min-width: 70px;
}
.no-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
color: var(--text-muted);
}
.no-preview i {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.5;
}
.error-message {
color: var(--error-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 2rem;
text-align: center;
}
.error-message i {
font-size: 3rem;
margin-bottom: 1rem;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modalIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-out {
animation: fadeOut 0.3s forwards;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.browser-header {
flex-direction: column;
align-items: flex-start;
}
.browser-actions {
margin-top: 1rem;
}
}

View file

@ -17,20 +17,14 @@
{% block extra_css %}{% endblock %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Close alert buttons
document.querySelectorAll('.alert .close').forEach(function (alert) {
alert.addEventListener('click', function () {
this.parentElement.style.display = 'none';
});
});
// Dark mode toggle
// Dark mode toggle button
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
function setColorScheme(scheme) {
document.documentElement.setAttribute('data-theme', scheme);
document.documentElement.setAttribute('color-scheme', scheme);
localStorage.setItem('color-scheme', scheme);
}
@ -50,7 +44,6 @@
});
darkModeToggle.checked = getColorScheme() === 'dark';
}
});
</script>
{% block extra_js %}{% endblock %}
@ -73,7 +66,7 @@
<li><a href="{{ url_for('auth.login') }}"><i class="fas fa-sign-in-alt"></i> Login</a></li>
{% endif %}
<li>
<button id="darkModeToggle" class="toggle-button" aria-label="Toggle dark mode">
<button id="darkModeToggle" class="toggle-button">
<i class="fas fa-moon"></i>
</button>
</li>

View file

@ -176,6 +176,51 @@
if (emptyNewFolderBtn) {
emptyNewFolderBtn.addEventListener('click', showNewFolderPrompt);
}
// Setup dismiss
const closeBtn = alert.querySelector('.close');
closeBtn.addEventListener('click', function () {
alert.classList.add('fade-out');
setTimeout(() => {
alert.remove();
}, 300);
});
// Auto dismiss
setTimeout(() => {
if (alert.parentNode) {
alert.classList.add('fade-out');
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 300);
}
}, 5000);
});
// Helper functions
function formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDate(dateString) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
return date.toLocaleString();
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
{% endblock %}

View file

@ -0,0 +1,47 @@
{% if folders or files %}
<div class="files-grid grid-view">
{% if folders %}
{% for folder in folders %}
<a href="{{ url_for('files.browser', folder_id=folder.id) }}" class="folder-item" data-id="{{ folder.id }}">
<div class="item-icon">
<i class="fas fa-folder"></i>
</div>
<div class="item-info">
<div class="item-name">{{ folder.name }}</div>
<div class="item-details">
<span class="item-count">{{ folder.files.count() }} items</span>
<span class="item-date">{{ folder.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
{% endif %}
{% if files %}
{% for file in files %}
<a href="{{ url_for('files.download', file_id=file.id) }}" class="file-item" data-id="{{ file.id }}">
<div class="item-icon">
<i class="fas {{ file_icon(file.mime_type, file.name) }}"></i>
</div>
<div class="item-info">
<div class="item-name">{{ file.name }}</div>
<div class="item-details">
<span class="item-size">{{ format_size(file.size) }}</span>
<span class="item-date">{{ file.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</a>
{% endfor %}
{% endif %}
</div>
{% else %}
<div class="empty-folder">
<div class="empty-icon">
<i class="fas fa-folder-open"></i>
</div>
<div class="empty-message">
<h3>This folder is empty</h3>
<p>Upload files or create a folder to get started</p>
</div>
</div>
{% endif %}

View file

@ -8,42 +8,44 @@
{% block content %}
<section class="upload-container">
<div class="upload-header">
<h2>Upload Files</h2>
<div class="upload-tabs">
<button class="tab-btn active" data-tab="file-tab">Files</button>
<button class="tab-btn" data-tab="folder-tab">Folder</button>
</div>
<div class="upload-location">
<p>
Uploading to:
<span>Uploading to:</span>
{% if parent_folder %}
<a href="{{ url_for('files.browser', folder_id=parent_folder.id) }}">{{ parent_folder.name }}</a>
{% else %}
<a href="{{ url_for('files.browser') }}">Root</a>
{% endif %}
</p>
</div>
</div>
<div class="tab-content active" id="file-tab">
<form id="file-upload-form" enctype="multipart/form-data" class="upload-form">
<input type="hidden" name="folder_id" value="{{ parent_folder.id if parent_folder else '' }}">
<div class="upload-dropzone" id="file-dropzone">
<div class="upload-dropzone" id="dropzone">
<div class="upload-icon-wrapper">
<i class="fas fa-cloud-upload-alt upload-icon"></i>
<p>Drag & drop files here to start uploading</p>
<p>or</p>
</div>
<div class="upload-text">
<p class="upload-primary-text">Drag & drop files or folders here</p>
<p class="upload-secondary-text">or</p>
<div class="upload-buttons">
<label class="btn primary">
<i class="fas fa-file"></i> Select Files
<input type="file" name="files[]" multiple id="file-input" style="display: none">
</label>
<label class="btn">
<i class="fas fa-folder"></i> Select Folder
<input type="file" name="folders[]" webkitdirectory directory id="folder-input"
style="display: none">
</label>
</div>
<p class="upload-hint">Files will upload automatically when dropped or selected</p>
</div>
</div>
<div class="upload-progress-container">
<h4>Upload Progress</h4>
<div class="upload-progress-container" id="progress-container" style="display: none;">
<h3>Upload Progress</h3>
<div class="progress-overall">
<div class="progress-label">
<div class="progress-header">
<span>Overall Progress</span>
<span id="progress-percentage">0%</span>
</div>
@ -51,90 +53,511 @@
<div class="progress-bar" id="progress-bar"></div>
</div>
</div>
<div class="upload-stats">
<div class="stat">
<span class="stat-label">Speed:</span>
<span class="stat-value" id="upload-speed">0 KB/s</span>
<div class="stat-item">
<i class="fas fa-tachometer-alt"></i>
<span id="upload-speed">0 KB/s</span>
</div>
<div class="stat">
<span class="stat-label">Uploaded:</span>
<span class="stat-value" id="uploaded-size">0 KB / 0 KB</span>
<div class="stat-item">
<i class="fas fa-file-upload"></i>
<span id="uploaded-size">0 KB / 0 KB</span>
</div>
<div class="stat">
<span class="stat-label">Remaining:</span>
<span class="stat-value" id="time-remaining">calculating...</span>
<div class="stat-item">
<i class="fas fa-clock"></i>
<span id="time-remaining">calculating...</span>
</div>
</div>
</div>
<div class="selected-files">
<h4>Files</h4>
<div class="file-list" id="file-list"></div>
</div>
<div class="form-actions">
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}"
class="btn">Back to Browser</a>
</div>
</form>
</div>
<div class="tab-content" id="folder-tab">
<form id="folder-upload-form" enctype="multipart/form-data" class="upload-form">
<input type="hidden" name="folder_id" value="{{ parent_folder.id if parent_folder else '' }}">
<div class="upload-dropzone folder-dropzone" id="folder-dropzone">
<i class="fas fa-folder-open upload-icon"></i>
<p>Select a folder to upload</p>
<p>(Some browsers may not fully support folder drag & drop)</p>
<label class="btn primary">
<i class="fas fa-folder"></i> Select Folder
<input type="file" name="files[]" webkitdirectory directory multiple id="folder-input"
style="display: none">
</label>
</div>
<div class="upload-progress-container">
<h4>Upload Progress</h4>
<div class="progress-overall">
<div class="progress-label">
<span>Overall Progress</span>
<span id="folder-progress-percentage">0%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="folder-progress-bar"></div>
</div>
</div>
<div class="upload-stats">
<div class="stat">
<span class="stat-label">Speed:</span>
<span class="stat-value" id="folder-upload-speed">0 KB/s</span>
</div>
<div class="stat">
<span class="stat-label">Uploaded:</span>
<span class="stat-value" id="folder-uploaded-size">0 KB / 0 KB</span>
</div>
<div class="stat">
<span class="stat-label">Remaining:</span>
<span class="stat-value" id="folder-time-remaining">calculating...</span>
</div>
<div class="upload-list" id="upload-list">
<h3>Files (<span id="file-count">0</span>)</h3>
<div id="file-items" class="file-items"></div>
<div id="empty-message" class="empty-message">
<p>No files selected yet</p>
</div>
</div>
<div class="selected-files">
<h4>Folder Contents</h4>
<div class="file-list" id="folder-file-list"></div>
</div>
<div class="form-actions">
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}"
class="btn">Back to Browser</a>
</div>
</form>
<div class="upload-actions">
<a href="{{ url_for('files.browser', folder_id=parent_folder.id if parent_folder else None) }}" class="btn">
Cancel
</a>
<button id="clear-button" class="btn" disabled>Clear All</button>
</div>
</section>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('file-input');
const folderInput = document.getElementById('folder-input');
const uploadList = document.getElementById('file-items');
const emptyMessage = document.getElementById('empty-message');
const fileCount = document.getElementById('file-count');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressPercentage = document.getElementById('progress-percentage');
const uploadSpeed = document.getElementById('upload-speed');
const uploadedSize = document.getElementById('uploaded-size');
const timeRemaining = document.getElementById('time-remaining');
const clearButton = document.getElementById('clear-button');
// Upload tracking
let uploadQueue = [];
let currentUploads = 0;
let totalUploaded = 0;
let totalSize = 0;
let uploadStartTime = 0;
let lastUploadedBytes = 0;
let uploadUpdateInterval = null;
const MAX_CONCURRENT_UPLOADS = 3;
const folderId = {{ parent_folder.id if parent_folder else 'null' }
};
// Setup event listeners
dropzone.addEventListener('dragover', function (e) {
e.preventDefault();
this.classList.add('highlight');
});
dropzone.addEventListener('dragleave', function (e) {
e.preventDefault();
this.classList.remove('highlight');
});
dropzone.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('highlight');
// Handle dropped files
const items = e.dataTransfer.items;
if (items && items.length > 0) {
// Check if this is a folder drop from file explorer
const containsDirectories = Array.from(items).some(item => {
return item.webkitGetAsEntry && item.webkitGetAsEntry().isDirectory;
});
if (containsDirectories) {
processDroppedItems(items);
} else {
handleFiles(e.dataTransfer.files);
}
}
});
fileInput.addEventListener('change', function () {
handleFiles(this.files);
});
folderInput.addEventListener('change', function () {
handleFolderUpload(this.files);
});
clearButton.addEventListener('click', function () {
resetUploadState();
});
function processDroppedItems(items) {
const fileList = [];
let pendingDirectories = 0;
function traverseFileTree(entry, path = '') {
if (entry.isFile) {
entry.file(file => {
file.relativePath = path + file.name;
fileList.push(file);
if (pendingDirectories === 0 && entry.isFile) {
handleFiles(fileList);
}
});
} else if (entry.isDirectory) {
pendingDirectories++;
const dirReader = entry.createReader();
const readEntries = () => {
dirReader.readEntries(entries => {
if (entries.length > 0) {
for (let i = 0; i < entries.length; i++) {
traverseFileTree(entries[i], path + entry.name + '/');
}
readEntries(); // Continue reading if there might be more entries
} else {
pendingDirectories--;
if (pendingDirectories === 0) {
handleFiles(fileList);
}
}
});
};
readEntries();
}
}
for (let i = 0; i < items.length; i++) {
const entry = items[i].webkitGetAsEntry();
if (entry) {
traverseFileTree(entry);
}
}
}
function handleFiles(files) {
if (!files || files.length === 0) return;
clearButton.disabled = false;
emptyMessage.style.display = 'none';
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Add to queue
uploadQueue.push({
file: file,
relativePath: file.relativePath || null,
status: 'queued',
progress: 0
});
totalSize += file.size;
// Add to UI
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.dataset.index = uploadQueue.length - 1;
// Determine icon based on file type
const fileIcon = getFileIcon(file.name);
fileItem.innerHTML = `
<div class="file-icon">
<i class="fas ${fileIcon}"></i>
</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-path">${file.relativePath || 'No path'}</div>
<div class="file-size">${formatSize(file.size)}</div>
<div class="file-progress">
<div class="progress-bar-small" style="width: 0%"></div>
</div>
</div>
<div class="file-status">
<span class="status-indicator queued">Queued</span>
</div>
`;
uploadList.appendChild(fileItem);
}
fileCount.textContent = uploadQueue.length;
// Start upload process
startUpload();
}
function handleFolderUpload(files) {
if (!files || files.length === 0) return;
const fileArray = Array.from(files);
for (let i = 0; i < fileArray.length; i++) {
const file = fileArray[i];
file.relativePath = file.webkitRelativePath;
}
handleFiles(fileArray);
}
function startUpload() {
if (uploadQueue.length === 0 || currentUploads >= MAX_CONCURRENT_UPLOADS) return;
if (currentUploads === 0) {
// First upload - initialize tracking
uploadStartTime = Date.now();
lastUploadedBytes = 0;
progressContainer.style.display = 'block';
// Start progress update interval
uploadUpdateInterval = setInterval(updateUploadStats, 500);
}
// Find next queued file
const nextIndex = uploadQueue.findIndex(item => item.status === 'queued');
if (nextIndex === -1) return;
// Start uploading this file
uploadQueue[nextIndex].status = 'uploading';
currentUploads++;
// Update UI
const fileItem = document.querySelector(`.file-item[data-index="${nextIndex}"]`);
const statusIndicator = fileItem.querySelector('.status-indicator');
statusIndicator.className = 'status-indicator uploading';
statusIndicator.textContent = 'Uploading';
// Create FormData
const formData = new FormData();
formData.append('file', uploadQueue[nextIndex].file);
formData.append('folder_id', folderId || '');
if (uploadQueue[nextIndex].relativePath) {
formData.append('relative_path', uploadQueue[nextIndex].relativePath);
}
// Create and configure XHR
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
const percentComplete = Math.round((e.loaded / e.total) * 100);
// Update file progress
uploadQueue[nextIndex].progress = percentComplete;
// Update file UI
const progressBar = fileItem.querySelector('.progress-bar-small');
progressBar.style.width = percentComplete + '%';
}
});
xhr.addEventListener('load', function () {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.success) {
// Upload successful
uploadQueue[nextIndex].status = 'complete';
statusIndicator.className = 'status-indicator complete';
statusIndicator.textContent = 'Complete';
totalUploaded += uploadQueue[nextIndex].file.size;
} else {
// Upload failed on server
uploadQueue[nextIndex].status = 'error';
statusIndicator.className = 'status-indicator error';
statusIndicator.textContent = 'Error: ' + (response.error || 'Server Error');
}
} catch (e) {
// JSON parse error
uploadQueue[nextIndex].status = 'error';
statusIndicator.className = 'status-indicator error';
statusIndicator.textContent = 'Error: Invalid response';
}
} else {
// HTTP error
uploadQueue[nextIndex].status = 'error';
statusIndicator.className = 'status-indicator error';
statusIndicator.textContent = 'Error: ' + xhr.status;
}
// One upload completed
currentUploads--;
// Check if all uploads complete
if (uploadQueue.every(item => item.status !== 'queued' && item.status !== 'uploading')) {
// All uploads complete
clearInterval(uploadUpdateInterval);
// Show completion notification
const successCount = uploadQueue.filter(item => item.status === 'complete').length;
const errorCount = uploadQueue.filter(item => item.status === 'error').length;
// Show notification message
if (errorCount === 0) {
showNotification(`Successfully uploaded ${successCount} files`, 'success');
} else {
showNotification(`Uploaded ${successCount} files, ${errorCount} failed`, 'warning');
}
} else {
// Start next upload
startUpload();
}
});
xhr.addEventListener('error', function () {
// Network error
uploadQueue[nextIndex].status = 'error';
statusIndicator.className = 'status-indicator error';
statusIndicator.textContent = 'Error: Network error';
currentUploads--;
startUpload(); // Try the next file
});
// Send the request
xhr.open('POST', '/files/upload_file');
xhr.send(formData);
// Try to start more uploads if possible
startUpload();
}
function updateUploadStats() {
if (currentUploads === 0) return;
// Calculate overall progress
let totalProgress = 0;
uploadQueue.forEach(item => {
if (item.status === 'complete') {
totalProgress += item.file.size;
} else if (item.status === 'uploading') {
totalProgress += (item.file.size * (item.progress / 100));
}
});
const overallPercentage = Math.round((totalProgress / totalSize) * 100);
// Update progress bar
progressBar.style.width = overallPercentage + '%';
progressPercentage.textContent = overallPercentage + '%';
// Calculate upload speed
const elapsed = (Date.now() - uploadStartTime) / 1000; // seconds
const bytesPerSecond = totalProgress / elapsed;
uploadSpeed.textContent = formatSize(bytesPerSecond) + '/s';
// Update uploaded size
uploadedSize.textContent = `${formatSize(totalProgress)} / ${formatSize(totalSize)}`;
// Calculate time remaining
const remainingBytes = totalSize - totalProgress;
if (bytesPerSecond > 0) {
const secondsRemaining = Math.round(remainingBytes / bytesPerSecond);
timeRemaining.textContent = formatTime(secondsRemaining);
} else {
timeRemaining.textContent = 'calculating...';
}
}
function resetUploadState() {
// Reset all upload tracking variables
uploadQueue = [];
currentUploads = 0;
totalUploaded = 0;
totalSize = 0;
clearInterval(uploadUpdateInterval);
// Reset UI
uploadList.innerHTML = '';
emptyMessage.style.display = 'block';
progressContainer.style.display = 'none';
fileCount.textContent = '0';
progressBar.style.width = '0%';
progressPercentage.textContent = '0%';
uploadSpeed.textContent = '0 KB/s';
uploadedSize.textContent = '0 KB / 0 KB';
timeRemaining.textContent = 'calculating...';
clearButton.disabled = true;
}
function showNotification(message, type) {
// Create alerts container if it doesn't exist
let alertsContainer = document.querySelector('.alerts');
if (!alertsContainer) {
alertsContainer = document.createElement('div');
alertsContainer.className = 'alerts';
document.body.appendChild(alertsContainer);
}
// Create alert
const alert = document.createElement('div');
alert.className = `alert ${type || 'info'}`;
alert.innerHTML = `
<div class="alert-content">${message}</div>
<button class="close" aria-label="Close">&times;</button>
`;
// Add to container
alertsContainer.appendChild(alert);
// Setup dismiss
const closeBtn = alert.querySelector('.close');
closeBtn.addEventListener('click', function () {
alert.classList.add('fade-out');
setTimeout(() => {
alert.remove();
}, 300);
});
// Auto dismiss
setTimeout(() => {
if (alert.parentNode) {
alert.classList.add('fade-out');
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 300);
}
}, 5000);
}
function formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatTime(seconds) {
if (seconds < 60) {
return seconds + ' seconds';
} else if (seconds < 3600) {
return Math.floor(seconds / 60) + ' minutes';
} else {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours + ' hours ' + minutes + ' minutes';
}
}
function getFileIcon(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
// Images
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(extension)) {
return 'fa-file-image';
}
// Videos
else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(extension)) {
return 'fa-file-video';
}
// Audio
else if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(extension)) {
return 'fa-file-audio';
}
// Documents
else if (['doc', 'docx', 'dot', 'dotx'].includes(extension)) {
return 'fa-file-word';
}
else if (['xls', 'xlsx', 'csv'].includes(extension)) {
return 'fa-file-excel';
}
else if (['ppt', 'pptx'].includes(extension)) {
return 'fa-file-powerpoint';
}
else if (['pdf'].includes(extension)) {
return 'fa-file-pdf';
}
// Archives
else if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
return 'fa-file-archive';
}
// Text
else if (['txt', 'rtf', 'md', 'log'].includes(extension)) {
return 'fa-file-alt';
}
// Code
else if (['html', 'css', 'js', 'php', 'py', 'java', 'c', 'cpp', 'h', 'xml', 'json', 'sql'].includes(extension)) {
return 'fa-file-code';
}
return 'fa-file';
}
});
</script>
{% endblock %}