mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-02 13:03:31 +00:00
- Reintroduced footer content in index.html to ensure proper display at the bottom of the page. - Updated styles.css to enhance footer visibility and layout, including adjustments to padding and opacity for better aesthetics. - Modified body and container styles to accommodate the footer without overlap, ensuring a cleaner user interface.
448 lines
30 KiB
HTML
448 lines
30 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{SITE_TITLE}} - Simple File Upload</title>
|
|
<link rel="stylesheet" href="{{BASE_URL}}styles.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
|
<link rel="manifest" href="{{BASE_URL}}manifest.json">
|
|
<link rel="icon" type="image/svg+xml" href="{{BASE_URL}}assets/icon.svg">
|
|
<script>window.BASE_URL = '{{BASE_URL}}';</script>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="theme-toggle-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<!-- Moon icon (shown in light mode) -->
|
|
<path class="moon" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
<!-- Sun icon (shown in dark mode) -->
|
|
<circle class="sun" cx="12" cy="12" r="5" style="display:none"/>
|
|
<line class="sun" x1="12" y1="1" x2="12" y2="3" style="display:none"/>
|
|
<line class="sun" x1="12" y1="21" x2="12" y2="23" style="display:none"/>
|
|
<line class="sun" x1="4.22" y1="4.22" x2="5.64" y2="5.64" style="display:none"/>
|
|
<line class="sun" x1="18.36" y1="18.36" x2="19.78" y2="19.78" style="display:none"/>
|
|
<line class="sun" x1="1" y1="12" x2="3" y2="12" style="display:none"/>
|
|
<line class="sun" x1="21" y1="12" x2="23" y2="12" style="display:none"/>
|
|
<line class="sun" x1="4.22" y1="19.78" x2="5.64" y2="18.36" style="display:none"/>
|
|
<line class="sun" x1="18.36" y1="5.64" x2="19.78" y2="4.22" style="display:none"/>
|
|
</svg>
|
|
</button>
|
|
<h1>{{SITE_TITLE}}</h1>
|
|
<div class="upload-container" id="dropZone">
|
|
<div class="upload-content">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="17 8 12 3 7 8"/>
|
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
</svg>
|
|
<p>Drag and drop files or folders here<br>or</p>
|
|
<input type="file" id="fileInput" multiple hidden>
|
|
<input type="file" id="folderInput" webkitdirectory directory multiple hidden>
|
|
<div class="button-group">
|
|
<button onclick="document.getElementById('fileInput').click()">Browse Files</button>
|
|
<button onclick="document.getElementById('folderInput').click()">Browse Folders</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="uploadProgress"></div> <!-- Original progress bar container -->
|
|
<div id="fileList" class="file-list"></div> <!-- Original file list container -->
|
|
<button id="uploadButton" class="upload-button" style="display: none;">Upload Files</button>
|
|
</div>
|
|
|
|
<script defer>
|
|
// *** MODIFIED CHUNK_SIZE for S3 compatibility ***
|
|
const CHUNK_SIZE = 1024 * 1024 * 5; // 5MB chunks
|
|
const RETRY_DELAY = 1000; // 1 second delay between retries
|
|
|
|
const MAX_RETRIES_STR = '{{MAX_RETRIES}}';
|
|
let maxRetries = 5;
|
|
if (MAX_RETRIES_STR && MAX_RETRIES_STR !== '{{MAX_RETRIES}}') {
|
|
const parsedRetries = parseInt(MAX_RETRIES_STR, 10);
|
|
if (!isNaN(parsedRetries) && parsedRetries >= 0) maxRetries = parsedRetries;
|
|
else console.warn(`Invalid MAX_RETRIES value "${MAX_RETRIES_STR}", defaulting to ${maxRetries}.`);
|
|
} else console.warn('MAX_RETRIES not injected by server, defaulting to 5.');
|
|
window.MAX_RETRIES = maxRetries;
|
|
console.log(`Max retries for chunk uploads: ${window.MAX_RETRIES}`);
|
|
|
|
const AUTO_UPLOAD_STR = '{{AUTO_UPLOAD}}';
|
|
const AUTO_UPLOAD = ['true', '1', 'yes'].includes(AUTO_UPLOAD_STR.toLowerCase());
|
|
|
|
// --- NEW: Variable to track active uploads ---
|
|
let activeUploadCount = 0;
|
|
|
|
function generateBatchId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }
|
|
function formatFileSize(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]; }
|
|
|
|
class FileUploader {
|
|
constructor(file, batchId) {
|
|
this.file = file;
|
|
this.batchId = batchId;
|
|
this.uploadId = null;
|
|
this.position = 0;
|
|
this.progressElement = null;
|
|
this.chunkSize = CHUNK_SIZE;
|
|
this.lastUploadedBytes = 0;
|
|
this.lastUploadTime = null;
|
|
this.uploadRate = 0;
|
|
this.maxRetries = window.MAX_RETRIES;
|
|
this.retryDelay = RETRY_DELAY;
|
|
// *** ADDED for S3/Adapter logic ***
|
|
this.partNumber = 1;
|
|
this.completed = false;
|
|
}
|
|
|
|
async start() {
|
|
try {
|
|
this.createProgressElement(); // Original: create separate progress bar
|
|
this.updateProgress(0);
|
|
await this.initUpload();
|
|
|
|
if (this.uploadId && this.uploadId.startsWith('zero-byte-')) {
|
|
console.log(`Zero-byte file ${this.file.name} handled by server init.`);
|
|
this.updateProgress(100);
|
|
this.completed = true;
|
|
return true;
|
|
}
|
|
|
|
if (this.uploadId && this.file.size > 0) {
|
|
await this.uploadChunks();
|
|
} else if (this.file.size === 0 && !this.completed) {
|
|
console.warn(`File ${this.file.name} is zero bytes, init didn't indicate completion. Assuming complete.`);
|
|
this.updateProgress(100);
|
|
this.completed = true;
|
|
}
|
|
return this.completed;
|
|
} catch (error) {
|
|
console.error(`Upload failed for ${this.file.webkitRelativePath || this.file.name}:`, error);
|
|
if (this.progressElement) {
|
|
this.progressElement.infoSpan.textContent = `Error: ${error.message}`;
|
|
this.progressElement.infoSpan.style.color = 'var(--danger-color)';
|
|
}
|
|
await this.cancelUploadOnServer();
|
|
this.completed = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async initUpload() {
|
|
const uploadPath = this.file.webkitRelativePath || this.file.name;
|
|
const consistentPath = uploadPath.replace(/\\/g, '/');
|
|
console.log(`[Uploader] Init for: ${consistentPath} (Size: ${this.file.size})`);
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (this.batchId) headers['X-Batch-ID'] = this.batchId;
|
|
const apiUrlPath = '/api/upload/init';
|
|
const fullApiUrl = window.BASE_URL + (apiUrlPath.startsWith('/') ? apiUrlPath.substring(1) : apiUrlPath);
|
|
|
|
const response = await fetch(fullApiUrl, {
|
|
method: 'POST', headers,
|
|
body: JSON.stringify({ filename: consistentPath, fileSize: this.file.size })
|
|
});
|
|
if (!response.ok) {
|
|
const errData = await response.json().catch(() => ({ error: `Server error ${response.status}` }));
|
|
throw new Error(errData.details || errData.error || `Init failed: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
if (!data.uploadId) throw new Error('Server did not return uploadId');
|
|
this.uploadId = data.uploadId;
|
|
console.log(`[Uploader] Init success. App Upload ID: ${this.uploadId}`);
|
|
}
|
|
|
|
async uploadChunks() {
|
|
if (!this.progressElement) this.createProgressElement(); // Ensure progress bar exists
|
|
|
|
while (this.position < this.file.size && !this.completed) {
|
|
const chunkStartPosition = this.position;
|
|
const chunk = await this.readChunk();
|
|
const currentPartNumber = this.partNumber;
|
|
|
|
try {
|
|
const result = await this.uploadChunkWithRetry(chunk, chunkStartPosition, currentPartNumber);
|
|
// Update original progress bar with server's progress
|
|
this.updateProgress(result.progress);
|
|
this.partNumber++;
|
|
if (result.completed) {
|
|
this.completed = true;
|
|
this.updateProgress(100); // Ensure it hits 100%
|
|
break;
|
|
}
|
|
} catch (error) {
|
|
console.error(`[Uploader] UploadChunks failed for Part ${this.partNumber}, File: ${this.file.name}`);
|
|
throw error;
|
|
}
|
|
}
|
|
if (!this.completed && this.position >= this.file.size) {
|
|
this.completed = true; this.updateProgress(100);
|
|
}
|
|
}
|
|
|
|
async readChunk() {
|
|
const start = this.position;
|
|
const end = Math.min(this.position + this.chunkSize, this.file.size);
|
|
const blob = this.file.slice(start, end);
|
|
this.position = end;
|
|
return await blob.arrayBuffer();
|
|
}
|
|
|
|
async uploadChunkWithRetry(chunk, chunkStartPosition, partNumber) {
|
|
const chunkApiUrlPath = `/api/upload/chunk/${this.uploadId}?partNumber=${partNumber}`; // *** ADDED partNumber ***
|
|
const fullChunkApiUrl = window.BASE_URL + (chunkApiUrlPath.startsWith('/') ? chunkApiUrlPath.substring(1) : chunkApiUrlPath);
|
|
let lastError = null;
|
|
|
|
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
try {
|
|
if (attempt > 0) {
|
|
console.warn(`[Uploader] Retrying Part ${partNumber} (Attempt ${attempt}/${this.maxRetries})`);
|
|
this.updateProgressElementInfo(`Retrying attempt ${attempt}...`, 'var(--warning-color)');
|
|
} else if (this.progressElement) { // Update info for first attempt
|
|
this.updateProgressElementInfo(`uploading part ${partNumber}...`);
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 60000); // 60s timeout
|
|
|
|
const response = await fetch(fullChunkApiUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/octet-stream', 'X-Batch-ID': this.batchId },
|
|
body: chunk, signal: controller.signal
|
|
});
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json(); // Contains { bytesReceived, progress, completed }
|
|
if (attempt > 0) console.log(`[Uploader] Part ${partNumber} success on retry ${attempt}.`);
|
|
else console.debug(`[Uploader] Part ${partNumber} uploaded successfully.`);
|
|
if(this.progressElement) this.updateProgressElementInfo('uploading...'); // Reset info
|
|
return data; // *** RETURN server data (has 'completed' flag) ***
|
|
} else {
|
|
let errorText = `Server error ${response.status}`; try { errorText = (await response.json()).error || errorText } catch(e){}
|
|
if (response.status === 404 && attempt > 0) {
|
|
console.warn(`[Uploader] 404 on retry (Part ${partNumber}), assuming completed.`);
|
|
this.completed = true; // Mark as completed
|
|
// this.updateProgress(100); // updateProgress is called from uploadChunks
|
|
return { completed: true, progress: 100, bytesReceived: this.file.size };
|
|
}
|
|
lastError = new Error(`Failed Part ${partNumber}: ${errorText}`);
|
|
console.error(`Attempt ${attempt} failed: ${lastError.message}`);
|
|
this.updateProgressElementInfo(`Attempt ${attempt} failed: ${response.statusText}`, 'var(--danger-color)');
|
|
}
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (error.name === 'AbortError') { console.error(`Part ${partNumber} Attempt ${attempt} timed out.`); this.updateProgressElementInfo(`Attempt ${attempt} timed out`, 'var(--danger-color)');}
|
|
else { console.error(`Part ${partNumber} Attempt ${attempt} network error: ${error.message}`); this.updateProgressElementInfo(`Attempt ${attempt} network error`, 'var(--danger-color)'); }
|
|
}
|
|
if (attempt < this.maxRetries) await new Promise(r => setTimeout(r, Math.min(this.retryDelay * Math.pow(2, attempt), 30000)));
|
|
}
|
|
console.error(`[Uploader] Part ${partNumber} failed permanently after ${this.maxRetries} retries.`);
|
|
this.updateProgressElementInfo(`Upload failed after ${this.maxRetries} retries`, 'var(--danger-color)');
|
|
throw lastError || new Error(`Part ${partNumber} failed after ${this.maxRetries} retries.`);
|
|
}
|
|
|
|
// --- Original Progress Bar UI Methods ---
|
|
createProgressElement() {
|
|
if (this.progressElement) return;
|
|
const container = document.createElement('div'); container.className = 'progress-container';
|
|
const label = document.createElement('div'); label.className = 'progress-label'; label.textContent = this.file.webkitRelativePath || this.file.name;
|
|
const progress = document.createElement('div'); progress.className = 'progress';
|
|
const bar = document.createElement('div'); bar.className = 'progress-bar';
|
|
const status = document.createElement('div'); status.className = 'progress-status';
|
|
const info = document.createElement('div'); info.className = 'progress-info'; info.textContent = 'initializing...';
|
|
const details = document.createElement('div'); details.className = 'progress-details'; details.textContent = `0 Bytes of ${formatFileSize(this.file.size)} (0.0%)`;
|
|
status.appendChild(info); status.appendChild(details); progress.appendChild(bar);
|
|
container.appendChild(label); container.appendChild(progress); container.appendChild(status);
|
|
document.getElementById('uploadProgress').appendChild(container);
|
|
this.progressElement = { container, bar, infoSpan: info, detailsSpan: details };
|
|
this.lastUploadTime = Date.now(); this.lastUploadedBytes = 0; this.uploadRate = 0;
|
|
}
|
|
updateProgress(percent) {
|
|
if (!this.progressElement) this.createProgressElement(); if (!this.progressElement) return;
|
|
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
this.progressElement.bar.style.width = `${clampedPercent}%`;
|
|
const currentTime = Date.now(); const timeDiff = (currentTime - (this.lastUploadTime || currentTime)) / 1000;
|
|
const bytesDiff = this.position - this.lastUploadedBytes; // Use this.position for rate too
|
|
if (timeDiff > 0.1 && bytesDiff > 0) { this.uploadRate = bytesDiff / timeDiff; this.lastUploadedBytes = this.position; this.lastUploadTime = currentTime; }
|
|
else if (timeDiff > 5) { this.uploadRate = 0; }
|
|
let rateText = 'Calculating...';
|
|
if (this.uploadRate > 0) { const u=['B/s','KB/s','MB/s','GB/s']; let i=0,r=this.uploadRate; while(r>=1024&&i<u.length-1){r/=1024;i++;} rateText=`${r.toFixed(1)} ${u[i]}`; }
|
|
else if (this.position > 0 || clampedPercent > 0) { rateText = '0.0 B/s'; }
|
|
const statusText = clampedPercent >= 100 ? 'complete' : 'uploading...';
|
|
if (!this.progressElement.infoSpan.textContent.startsWith('Retry') && !this.progressElement.infoSpan.textContent.startsWith('Attempt') && !this.progressElement.infoSpan.textContent.startsWith('Error')) {
|
|
this.updateProgressElementInfo(`${rateText} · ${statusText}`);
|
|
}
|
|
this.progressElement.detailsSpan.textContent = `${formatFileSize(this.position)} of ${formatFileSize(this.file.size)} (${clampedPercent.toFixed(1)}%)`;
|
|
if (clampedPercent === 100) {
|
|
this.progressElement.container.style.opacity = '0.5'; // Original had fade out
|
|
setTimeout(() => { if (this.progressElement && this.progressElement.container) { this.progressElement.container.remove(); this.progressElement = null; }}, 2000);
|
|
}
|
|
}
|
|
updateProgressElementInfo(message, color = '') { if (this.progressElement && this.progressElement.infoSpan) { this.progressElement.infoSpan.textContent = message; this.progressElement.infoSpan.style.color = color; }}
|
|
async cancelUploadOnServer() { if (!this.uploadId || this.completed || this.uploadId.startsWith('zero-byte-')) return; console.log(`[Uploader] Server cancel for ${this.uploadId}`); try { const p=`/api/upload/cancel/${this.uploadId}`; const u=window.BASE_URL+(p.startsWith('/')?p.substring(1):p); fetch(u,{method:'POST'}).catch(e=>console.warn('Cancel req failed:',e));}catch(e){console.warn('Cancel init err:',e);}}
|
|
}
|
|
|
|
// --- Original UI Handlers and Logic ---
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const folderInput = document.getElementById('folderInput');
|
|
const fileListDiv = document.getElementById('fileList'); // Original div for list
|
|
const uploadButton = document.getElementById('uploadButton');
|
|
let filesToUpload = []; // Use a different name than original `files` for clarity
|
|
|
|
async function getAllFileEntries(dataTransferItems) { /* ... (original implementation from previous message) ... */
|
|
console.debug('Starting getAllFileEntries with items:', Array.from(dataTransferItems).map(item => ({ kind: item.kind, type: item.type })));
|
|
let fileEntries = []; let rootFolderName = null;
|
|
async function traverseEntry(entry, path = '') {
|
|
if (entry.isFile) {
|
|
const file = await new Promise((resolve, reject) => entry.file(f => {
|
|
const fileWithPath = new File([f], entry.name, { type: f.type, lastModified: f.lastModified });
|
|
const fullPath = path ? `${path}/${entry.name}` : entry.name;
|
|
Object.defineProperty(fileWithPath, 'webkitRelativePath', { value: fullPath.replace(/\\/g, '/'), writable: false, configurable: true, enumerable: true });
|
|
resolve(fileWithPath);
|
|
}, reject));
|
|
fileEntries.push(file);
|
|
} else if (entry.isDirectory) {
|
|
if (!path && !rootFolderName) rootFolderName = entry.name;
|
|
const dirReader = entry.createReader(); let entries = [];
|
|
let readEntries = await new Promise((resolve, reject) => { const readBatch = () => dirReader.readEntries(batch => batch.length > 0 ? (entries=entries.concat(batch), readBatch()) : resolve(entries), reject); readBatch(); });
|
|
const dirPath = path ? `${path}/${entry.name}` : entry.name;
|
|
for (const childEntry of entries) await traverseEntry(childEntry, dirPath);
|
|
}
|
|
}
|
|
try {
|
|
const entryPromises = Array.from(dataTransferItems).map(item => item.webkitGetAsEntry()).filter(Boolean).map(entry => traverseEntry(entry));
|
|
await Promise.all(entryPromises);
|
|
fileEntries.sort((a, b) => (a.webkitRelativePath || a.name).localeCompare(b.webkitRelativePath || b.name));
|
|
return fileEntries;
|
|
} catch (error) { console.error('Error in getAllFileEntries:', error); throw error; }
|
|
}
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(ev => { dropZone.addEventListener(ev, preventDefaults, false); document.body.addEventListener(ev, preventDefaults, false); });
|
|
['dragenter', 'dragover'].forEach(ev => dropZone.addEventListener(ev, highlight, false));
|
|
['dragleave', 'drop'].forEach(ev => dropZone.addEventListener(ev, unhighlight, false));
|
|
dropZone.addEventListener('drop', handleDrop);
|
|
fileInput.addEventListener('change', handleFilesFromInput);
|
|
folderInput.addEventListener('change', handleFilesFromInput);
|
|
uploadButton.addEventListener('click', startUploads);
|
|
|
|
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
|
|
function highlight() { dropZone.classList.add('highlight'); }
|
|
function unhighlight() { dropZone.classList.remove('highlight'); }
|
|
|
|
async function handleDrop(e) {
|
|
const items = e.dataTransfer.items;
|
|
fileListDiv.innerHTML = ''; // Clear old list display
|
|
uploadButton.style.display = 'none';
|
|
const loadingItem = document.createElement('div'); loadingItem.className = 'file-item loading'; loadingItem.textContent = 'Processing dropped items...'; fileListDiv.appendChild(loadingItem);
|
|
try {
|
|
let newFiles;
|
|
if (items && items.length > 0 && items[0].webkitGetAsEntry) newFiles = await getAllFileEntries(items);
|
|
else newFiles = [...e.dataTransfer.files].filter(f => f.size >= 0);
|
|
if (newFiles.length === 0) { loadingItem.textContent = 'No files found.'; setTimeout(() => loadingItem.remove(), 2000); return; }
|
|
filesToUpload = newFiles; updateFileList();
|
|
if (AUTO_UPLOAD) startUploads(); else if (filesToUpload.length > 0) uploadButton.style.display = 'block';
|
|
} catch (error) { console.error('Error handling drop:', error); loadingItem.textContent = `Error: ${error.message}`; loadingItem.style.color = 'var(--danger-color)'; setTimeout(() => {loadingItem.remove(); updateFileList();}, 3000); filesToUpload = []; }
|
|
finally { if (loadingItem.parentNode === fileListDiv && filesToUpload.length > 0) loadingItem.remove(); } // Remove loading only if files are shown
|
|
}
|
|
function handleFilesFromInput(e) {
|
|
const input = e.target; const selectedFiles = [...input.files];
|
|
if (input.id === 'folderInput' && selectedFiles.length > 0 && !('webkitRelativePath' in selectedFiles[0])) { alert('Folder upload not fully supported.'); filesToUpload = []; }
|
|
else filesToUpload = selectedFiles.filter(f => f.size >= 0);
|
|
updateFileList();
|
|
if (AUTO_UPLOAD && filesToUpload.length > 0) startUploads(); else if (filesToUpload.length > 0) uploadButton.style.display = 'block'; else uploadButton.style.display = 'none';
|
|
input.value = '';
|
|
}
|
|
|
|
function updateFileList() { // Original simple list display
|
|
console.debug('Updating original file list UI for', filesToUpload.length, 'files');
|
|
fileListDiv.innerHTML = '';
|
|
if (filesToUpload.length === 0) {
|
|
fileListDiv.innerHTML = '<div class="file-item placeholder">No files selected.</div>';
|
|
uploadButton.style.display = 'none';
|
|
return;
|
|
}
|
|
filesToUpload.forEach(file => {
|
|
const fileItem = document.createElement('div');
|
|
fileItem.className = 'file-item';
|
|
const displayName = file.webkitRelativePath || file.name;
|
|
fileItem.innerHTML = `📄 ${displayName} (${formatFileSize(file.size)})`;
|
|
fileListDiv.appendChild(fileItem);
|
|
});
|
|
uploadButton.style.display = (!AUTO_UPLOAD && filesToUpload.length > 0) ? 'block' : 'none';
|
|
}
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.file-list { /* Original styles for the list container */ margin-top: 20px; display: flex; flex-direction: column; gap: 10px; }
|
|
.file-item { background: var(--container-bg); padding: 10px 15px; border-radius: 5px; text-align: left; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.file-item.placeholder { text-align: center; opacity: 0.6; box-shadow: none; background: transparent; border: none; } /* Ensure placeholder has no border if list had one */
|
|
.file-item.loading { text-align: center; padding: 15px; background: var(--container-bg); border-radius: 5px; animation: pulse 1.5s infinite; }
|
|
@keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } }
|
|
/* Styles for the separate progress bars, from original */
|
|
#uploadProgress { margin: 20px 0; display: flex; flex-direction: column; gap: 15px; }
|
|
.progress-container { background: var(--container-bg); padding: 15px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); transition: opacity 0.5s ease-out; }
|
|
.progress-label { text-align: left; margin-bottom: 8px; color: var(--text-color); font-size: 0.9rem; }
|
|
.progress-status { display: flex; justify-content: space-between; align-items: center; font-size: 0.8rem; color: var(--text-color); opacity: 0.8; margin-top: 8px; }
|
|
.progress-info { text-align: left; } .progress-details { text-align: right; }
|
|
.progress { background: var(--progress-bg); border-radius: 10px; height: 8px; overflow: hidden; margin-top: 8px; margin-bottom: 8px; }
|
|
.progress-bar { height: 100%; background: var(--highlight-color); transition: width 0.3s ease; }
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
async function startUploads() {
|
|
if (filesToUpload.length === 0) { Toastify({ text: "No files selected.", duration: 3000 }).showToast(); return; }
|
|
uploadButton.disabled = true; uploadButton.textContent = 'Uploading...';
|
|
document.getElementById('uploadProgress').innerHTML = ''; // Clear old progress bars
|
|
|
|
const batchId = generateBatchId();
|
|
let successfulUploads = 0, failedUploads = 0;
|
|
|
|
for (const file of filesToUpload) {
|
|
// --- NEW: Increment active upload counter ---
|
|
activeUploadCount++;
|
|
const uploader = new FileUploader(file, batchId);
|
|
try {
|
|
if (await uploader.start()) successfulUploads++;
|
|
else failedUploads++;
|
|
}
|
|
catch (error) {
|
|
console.error(`Unhandled error for ${file.name}:`, error);
|
|
failedUploads++;
|
|
} finally {
|
|
// --- NEW: Decrement active upload counter ---
|
|
activeUploadCount--;
|
|
}
|
|
}
|
|
|
|
const totalFiles = filesToUpload.length;
|
|
let msg = `Uploaded ${successfulUploads} of ${totalFiles} files`;
|
|
let bg = successfulUploads === totalFiles ? "#4CAF50" : (successfulUploads > 0 ? "#ff9800" : "#f44336");
|
|
Toastify({ text: msg, duration: 3000, gravity: "bottom", position: "right", style: { background: bg } }).showToast();
|
|
|
|
filesToUpload = []; updateFileList();
|
|
uploadButton.disabled = false; uploadButton.textContent = 'Upload Files'; uploadButton.style.display = 'none';
|
|
fileInput.value = ''; folderInput.value = '';
|
|
}
|
|
|
|
function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); const m=document.querySelectorAll('.theme-toggle-icon .moon'); const s=document.querySelectorAll('.theme-toggle-icon .sun'); if(theme==='dark'){m.forEach(p=>p.style.display='none');s.forEach(p=>p.style.display='');}else{m.forEach(p=>p.style.display='');s.forEach(p=>p.style.display='none');} }
|
|
function toggleTheme() { const c=document.documentElement.getAttribute('data-theme'); setTheme(c==='dark'?'light':'dark'); }
|
|
const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; setTheme(savedTheme || (prefersDark ? 'dark' : 'light'));
|
|
updateFileList(); // Initialize list on load
|
|
|
|
// --- NEW: beforeunload event listener ---
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (activeUploadCount > 0) {
|
|
// Standard message for the confirmation dialog
|
|
const confirmationMessage = 'Uploads are in progress. If you leave this page, ongoing uploads will be interrupted. Are you sure you want to leave?';
|
|
|
|
// For modern browsers:
|
|
e.returnValue = confirmationMessage;
|
|
// For older browsers:
|
|
return confirmationMessage;
|
|
}
|
|
});
|
|
</script>
|
|
<footer>
|
|
{{FOOTER_CONTENT}}
|
|
</footer>
|
|
</body>
|
|
</html> |