Files
DumbDrop/public/index.html
greirson e4143c38db fix(footer): Restore footer content in index.html and adjust styles for improved layout
- 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.
2025-05-06 17:03:51 -07:00

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>