public/index.html
Rama master · réplica de lectura del repositorio original.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Manager Server</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
light: '#4895ef',
DEFAULT: '#4361ee',
dark: '#3f37c9',
},
},
},
},
}
</script>
</head>
<body class="bg-gray-50 text-gray-800 min-h-screen">
<!-- Header -->
<header class="bg-gradient-to-r from-primary-dark to-primary-light text-white py-6 px-4 shadow-md">
<div class="max-w-5xl mx-auto">
<div class="flex items-center justify-center">
<i class="fas fa-file-alt text-2xl mr-3 opacity-90"></i>
<h1 class="text-2xl font-semibold">File Manager Server</h1>
</div>
<p class="text-center text-white/80 mt-1">Upload and download files from any folder on your server</p>
<div class="text-center mt-2">
<a href="https://github.com/p32929/node_file_manager_server" target="_blank" rel="noopener noreferrer" class="inline-flex items-center text-white/90 hover:text-white text-sm bg-white/10 px-3 py-1 rounded-full transition-colors">
<i class="fab fa-github mr-2"></i>
<span>Source Code</span>
</a>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto p-4 md:p-6">
<!-- Explorer card -->
<div id="explorerCard" class="bg-white rounded-xl shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div class="flex items-center justify-between bg-gray-50 px-4 py-3 border-b border-gray-100">
<h2 class="text-lg font-medium text-gray-800">File Explorer</h2>
<button
onclick="showHelp()"
class="text-gray-500 hover:text-gray-700 bg-white border border-gray-200 px-3 py-1 rounded-md text-sm transition-colors flex items-center gap-1.5 hover:bg-gray-50">
<i class="fas fa-question-circle text-xs"></i>
<span>Help</span>
</button>
</div>
<div class="p-4 md:p-5">
<!-- Path navigation -->
<div id="breadcrumb" class="text-sm text-primary-dark mb-4 p-2 bg-gray-50 rounded-md overflow-x-auto whitespace-nowrap"></div>
<!-- Folder listing -->
<div id="folderContainer" class="min-h-[150px] border border-gray-100 rounded-lg p-4 bg-gray-50/50">
<div class="flex items-center justify-center h-[120px] text-gray-500">
<div class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin text-primary"></i>
<span>Loading folders...</span>
</div>
</div>
</div>
<!-- Navigation buttons -->
<div class="flex flex-wrap gap-2 mt-4">
<button
onclick="goBack()"
class="bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 px-4 py-2 rounded-lg flex items-center gap-1.5 transition-colors">
<i class="fas fa-arrow-left text-xs"></i>
<span>Back</span>
</button>
<button
onclick="goUp()"
class="bg-white text-gray-700 border border-gray-200 hover:bg-gray-50 px-4 py-2 rounded-lg flex items-center gap-1.5 transition-colors">
<i class="fas fa-arrow-up text-xs"></i>
<span>Up</span>
</button>
<button
onclick="selectCurrentFolder()"
class="bg-primary hover:bg-primary-dark text-white px-4 py-2 rounded-lg flex items-center gap-1.5 transition-colors ml-auto">
<i class="fas fa-check text-xs"></i>
<span>Select This Folder</span>
</button>
</div>
</div>
</div>
<!-- Upload card -->
<div id="uploadCard" class="bg-white rounded-xl shadow-sm border border-gray-100 mb-6 overflow-hidden" style="display:none;">
<div class="bg-gray-50 px-4 py-3 border-b border-gray-100">
<h2 class="text-lg font-medium text-gray-800">Upload File</h2>
</div>
<div class="p-4 md:p-5">
<!-- Selected path -->
<div class="mb-4 px-3 py-2 bg-gray-50 rounded-md text-sm">
<span class="text-gray-500">Selected folder:</span>
<span id="selectedFolderPath" class="text-gray-900 font-medium ml-1"></span>
</div>
<!-- Drop zone -->
<div
id="dropZone"
onclick="document.getElementById('fileInput').click()"
class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer transition-colors hover:border-primary hover:bg-blue-50/30 group">
<div class="flex flex-col items-center">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-300 group-hover:text-primary transition-colors mb-3"></i>
<p class="text-gray-500 group-hover:text-gray-700">Click or drag & drop a file here to upload</p>
<p class="text-xs text-gray-400 mt-2">Maximum file size depends on your server configuration</p>
</div>
</div>
<!-- Upload buttons -->
<div class="flex mt-4 gap-3 justify-center">
<div>
<input type="file" id="fileInput" class="hidden" onchange="onFileSelected()" />
<button
onclick="document.getElementById('fileInput').click()"
class="bg-white border border-gray-200 hover:bg-gray-50 text-gray-700 px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
<i class="fas fa-file"></i>
<span>Select File</span>
</button>
</div>
<div>
<input type="file" id="folderInput" class="hidden" webkitdirectory directory multiple onchange="onFolderSelected()" />
<button
onclick="document.getElementById('folderInput').click()"
class="bg-white border border-gray-200 hover:bg-gray-50 text-gray-700 px-4 py-2 rounded-lg flex items-center gap-2 transition-colors">
<i class="fas fa-folder"></i>
<span>Select Folder</span>
</button>
</div>
</div>
<!-- File info -->
<div id="fileInfo" class="mt-4"></div>
<!-- Upload button -->
<button
id="startUploadBtn"
onclick="startUpload()"
class="mt-4 bg-primary hover:bg-primary-dark text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors hidden">
<i class="fas fa-upload"></i>
<span>Upload</span>
</button>
<!-- Progress bar -->
<div id="uploadProgress" class="mt-5 hidden">
<div class="flex justify-between text-xs text-gray-500 mb-1">
<span>Progress</span>
<span id="uploadPercent">0%</span>
</div>
<div class="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
<div id="uploadProgressBar" class="h-full w-0 bg-green-500 transition-all duration-300"></div>
</div>
<div id="uploadSpeed" class="text-sm text-gray-600 mt-2"></div>
<div id="uploadStatus" class="text-sm text-gray-600 mt-2"></div>
<!-- Cancel button -->
<button
id="cancelUploadBtn"
onclick="cancelUpload()"
class="mt-4 bg-red-500 hover:bg-red-600 text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors hidden">
<i class="fas fa-times"></i>
<span>Cancel Upload</span>
</button>
</div>
</div>
</div>
<!-- Files card -->
<div id="filesCard" class="bg-white rounded-xl shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div class="bg-gray-50 px-4 py-3 border-b border-gray-100 flex justify-between items-center">
<h2 class="text-lg font-medium text-gray-800">Files</h2>
<div class="flex gap-2">
<button id="downloadSelectedBtn" onclick="downloadSelected()" disabled
class="bg-primary text-white px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition-colors opacity-50 cursor-not-allowed text-sm">
<i class="fas fa-download text-xs"></i>
<span>Download Selected</span>
</button>
<button onclick="refreshFiles()" class="text-gray-500 hover:text-gray-700 bg-white border border-gray-200 px-3 py-1.5 rounded-lg text-sm transition-colors flex items-center gap-1.5">
<i class="fas fa-sync-alt text-xs"></i>
<span>Refresh</span>
</button>
</div>
</div>
<div class="p-4 md:p-5">
<!-- Selected path and stats -->
<div class="flex flex-wrap justify-between items-center mb-4 px-3 py-2 bg-gray-50 rounded-md text-sm">
<div>
<span class="text-gray-500">Current folder:</span>
<span id="fileListPath" class="text-gray-900 font-medium ml-1"></span>
</div>
<div id="fileStats" class="text-gray-500 mt-1 md:mt-0">
<span id="fileCount">0 files</span>
<span class="mx-1">•</span>
<span id="totalSize">0 bytes</span>
</div>
</div>
<!-- Files listing -->
<div id="fileContainer" class="min-h-[200px] border border-gray-100 rounded-lg p-4 bg-gray-50/50">
<div class="flex items-center justify-center h-[200px] text-gray-500">
<div class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin text-primary"></i>
<span>Loading files...</span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Toast notifications -->
<div id="toast" class="fixed bottom-4 left-4 flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg text-white opacity-0 transform translate-y-8 transition-all duration-300 z-50 max-w-md">
<i class="fas fa-check-circle"></i>
<span id="toastMsg"></span>
</div>
<script>
const API_BASE = window.location.origin;
let currentPath = '/';
let historyStack = [];
let isUploading = false; // Track upload state
let cancelUploadRequested = false; // Track cancel request
let uploadStartTime = 0; // For speed calculation
let currentXhr = null; // Store the current XHR request
let selectedFiles = new Set(); // Track selected files for download
// Variables for folder upload tracking
let isUploadingFolder = false;
let folderFiles = [];
let currentFolderIndex = 0;
let folderUploadProgress = 0;
let folderName = '';
window.addEventListener('load', () => {
loadFolders('/');
// Also load files in the current directory
loadFiles('/');
});
function loadFolders(dir, pushHistory=true) {
const container = document.getElementById('folderContainer');
container.innerHTML = `
<div class="flex items-center justify-center h-[120px] text-gray-500">
<div class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin text-primary"></i>
<span>Loading folders...</span>
</div>
</div>`;
if (pushHistory && currentPath !== dir) {
historyStack.push(currentPath);
}
fetch(`/list-folders?path=${encodeURIComponent(dir)}`)
.then(r => r.json())
.then(data => {
if (data.error) {
container.innerHTML = `<div class="text-red-500 p-4">Error: ${data.error}</div>`;
return;
}
currentPath = data.path;
// Update file list for the new path
document.getElementById('fileListPath').textContent = currentPath;
loadFiles(currentPath);
if (data.isRoot) {
// On Windows root, fetch drives
return loadDrives();
}
updateBreadcrumb(currentPath);
if (!data.folders || data.folders.length === 0) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center h-[120px] text-gray-500">
<i class="fas fa-folder-open text-gray-300 text-4xl mb-2"></i>
<span>No subfolders here</span>
</div>`;
} else {
container.innerHTML = '';
const list = document.createElement('div');
list.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3';
data.folders.forEach(folderName => {
const div = document.createElement('div');
div.className = 'bg-white border border-gray-200 rounded-lg p-3 cursor-pointer hover:border-primary hover:shadow-sm transition-all flex items-center gap-2 truncate';
div.innerHTML = `
<i class="fas fa-folder text-yellow-500"></i>
<span class="truncate">${folderName}</span>
`;
div.onclick = () => {
loadFolders(pathJoin(currentPath, folderName));
};
list.appendChild(div);
});
container.appendChild(list);
}
})
.catch(err => {
container.innerHTML = `<div class="text-red-500 p-4">Failed to load folders: ${err}</div>`;
});
}
function loadDrives() {
fetch('/list-drives')
.then(r => r.json())
.then(data => {
updateBreadcrumb('My Computer');
const container = document.getElementById('folderContainer');
if (data.drives && data.drives.length) {
container.innerHTML = '';
const list = document.createElement('div');
list.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3';
data.drives.forEach(drv => {
const div = document.createElement('div');
div.className = 'bg-white border border-gray-200 rounded-lg p-3 cursor-pointer hover:border-primary hover:shadow-sm transition-all flex items-center gap-2';
div.innerHTML = `
<i class="fas fa-hdd text-blue-500"></i>
<span>${drv}</span>
`;
div.onclick = () => loadFolders(drv);
list.appendChild(div);
});
container.appendChild(list);
} else {
container.innerHTML = `
<div class="flex flex-col items-center justify-center h-[120px] text-gray-500">
<i class="fas fa-hdd text-gray-300 text-4xl mb-2"></i>
<span>No drives found</span>
</div>`;
}
})
.catch(err => {
document.getElementById('folderContainer').innerHTML = `
<div class="text-red-500 p-4">Error loading drives: ${err}</div>`;
});
}
function goBack() {
if (historyStack.length === 0) {
showToast('No previous folder', 'info');
return;
}
const prev = historyStack.pop();
loadFolders(prev, false);
}
function goUp() {
if (currentPath === '/' || currentPath === 'My Computer') {
showToast('Already at root', 'info');
return;
}
// If windows drive
if (/^[A-Za-z]:\\?$/.test(currentPath)) {
loadFolders('/'); // go back to "My Computer"
return;
}
const parts = currentPath.split(/[\\/]/).filter(Boolean);
parts.pop();
let upPath = parts.join('/');
if (upPath === '') upPath = '/';
loadFolders(upPath);
}
function updateBreadcrumb(label) {
const bc = document.getElementById('breadcrumb');
bc.innerHTML = `<span class="font-medium text-primary-dark hover:underline cursor-pointer">${label}</span>`;
}
function pathJoin(base, folder) {
if (base === '/') return '/' + folder;
// For Windows:
if (/^[A-Za-z]:\\?$/.test(base)) {
return base.replace(/\\?$/, '') + '\\' + folder;
}
// Normal path
return base.replace(/\/$/, '') + '/' + folder;
}
function selectCurrentFolder() {
document.getElementById('uploadCard').style.display = 'block';
document.getElementById('selectedFolderPath').textContent = currentPath;
document.getElementById('fileListPath').textContent = currentPath;
showToast('Folder selected: ' + currentPath, 'info');
// Reset upload UI
document.getElementById('fileInfo').innerHTML = '';
document.getElementById('startUploadBtn').style.display = 'none';
document.getElementById('uploadProgress').style.display = 'none';
document.getElementById('uploadStatus').innerHTML = '';
// Scroll to upload card
document.getElementById('uploadCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Set UI state during upload
function setUploadingState(uploading) {
isUploading = uploading;
// Disable or enable explorer card
const explorerCard = document.getElementById('explorerCard');
if (uploading) {
explorerCard.classList.add('opacity-50', 'pointer-events-none');
} else {
explorerCard.classList.remove('opacity-50', 'pointer-events-none');
}
// Show/hide cancel button and disable upload button
const startUploadBtn = document.getElementById('startUploadBtn');
const cancelUploadBtn = document.getElementById('cancelUploadBtn');
if (uploading) {
startUploadBtn.disabled = true;
startUploadBtn.classList.add('opacity-50', 'cursor-not-allowed');
cancelUploadBtn.classList.remove('hidden');
} else {
startUploadBtn.disabled = false;
startUploadBtn.classList.remove('opacity-50', 'cursor-not-allowed');
cancelUploadBtn.classList.add('hidden');
// Reset cancel flag
cancelUploadRequested = false;
}
// Disable drop zone
const dropZone = document.getElementById('dropZone');
if (uploading) {
dropZone.classList.add('opacity-50', 'pointer-events-none');
} else {
dropZone.classList.remove('opacity-50', 'pointer-events-none');
}
}
// Cancel ongoing upload
function cancelUpload() {
if (!isUploading) return;
cancelUploadRequested = true;
if (currentXhr) {
currentXhr.abort();
}
document.getElementById('uploadStatus').textContent = 'Cancelling upload...';
// Send request to server to delete temporary files
const fileName = selectedFile ? selectedFile.name : '';
fetch(`/cancel-upload?fileId=${fileId}&fileName=${encodeURIComponent(fileName)}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
setUploadingState(false);
document.getElementById('uploadStatus').textContent = 'Upload cancelled';
document.getElementById('uploadStatus').className = 'text-sm text-yellow-500 mt-2';
showToast('Upload cancelled', 'info');
})
.catch(error => {
console.error('Error cancelling upload:', error);
setUploadingState(false);
});
}
// Calculate and format upload speed
function calculateSpeed(bytesUploaded, elapsedMs) {
if (elapsedMs === 0) return 'N/A';
const bytesPerSecond = (bytesUploaded / elapsedMs) * 1000;
if (bytesPerSecond < 1024) {
return bytesPerSecond.toFixed(1) + ' B/s';
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(1) + ' KB/s';
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + ' MB/s';
} else {
return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + ' GB/s';
}
}
// File selection + upload
let selectedFile = null;
function onFileSelected() {
const input = document.getElementById('fileInput');
selectedFile = input.files[0];
if (!selectedFile) return;
document.getElementById('fileInfo').innerHTML = `
<div class="bg-blue-50 border border-blue-100 rounded-lg p-3 flex items-start">
<div class="bg-blue-100 rounded-lg p-2 text-blue-700">
<i class="fas fa-file-alt text-xl"></i>
</div>
<div class="ml-3">
<div class="font-medium">${selectedFile.name}</div>
<div class="text-sm text-gray-500 mt-0.5">${formatSize(selectedFile.size)}</div>
</div>
</div>`;
document.getElementById('startUploadBtn').style.display = 'flex';
}
function onFolderSelected() {
const input = document.getElementById('folderInput');
const files = input.files;
if (!files || files.length === 0) return;
// Get the folder name from the path of the first file
const firstPath = files[0].webkitRelativePath;
folderName = firstPath.split('/')[0];
// Store all files
folderFiles = Array.from(files);
// Update UI
document.getElementById('fileInfo').innerHTML = `
<div class="bg-blue-50 border border-blue-100 rounded-lg p-3 flex items-start">
<div class="bg-blue-100 rounded-lg p-2 text-blue-700">
<i class="fas fa-folder text-xl"></i>
</div>
<div class="ml-3">
<div class="font-medium">${folderName}</div>
<div class="text-sm text-gray-500 mt-0.5">${folderFiles.length} files (${formatSize(getTotalSize(folderFiles))})</div>
</div>
</div>`;
document.getElementById('startUploadBtn').style.display = 'flex';
// Set selectedFile to null to indicate we're using folder upload
selectedFile = null;
}
function getTotalSize(files) {
return files.reduce((total, file) => total + file.size, 0);
}
function startUpload() {
if (isUploading) {
showToast('Upload already in progress', 'info');
return;
}
const folder = currentPath;
if (!folder) {
showToast('No target folder', 'error');
return;
}
// Set upload start time for speed calculation
uploadStartTime = Date.now();
// Set uploading state
setUploadingState(true);
// Check if we're uploading a folder or a file
if (selectedFile) {
// Existing file upload logic
const MAX_SINGLE_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
if (selectedFile.size <= MAX_SINGLE_CHUNK_SIZE) {
// single chunk
uploadSingleChunk(selectedFile, folder);
} else {
// multi-chunk
uploadMultiChunk(selectedFile, folder);
}
} else if (folderFiles.length > 0) {
// Folder upload logic
isUploadingFolder = true;
currentFolderIndex = 0;
folderUploadProgress = 0;
showToast(`Uploading folder ${folderName}`, 'info');
document.getElementById('uploadProgress').style.display = 'block';
document.getElementById('uploadProgressBar').style.width = '0%';
document.getElementById('uploadPercent').textContent = '0%';
document.getElementById('uploadSpeed').textContent = 'Calculating...';
document.getElementById('uploadStatus').textContent = `Preparing to upload ${folderFiles.length} files...`;
// Create folder structure on server then start uploads
uploadNextFolderFile(folder);
} else {
showToast('No file or folder selected', 'error');
setUploadingState(false);
}
}
function uploadNextFolderFile(targetFolder) {
if (cancelUploadRequested) {
isUploadingFolder = false;
return;
}
if (currentFolderIndex >= folderFiles.length) {
// All files in folder uploaded
document.getElementById('uploadProgressBar').style.width = '100%';
document.getElementById('uploadPercent').textContent = '100%';
document.getElementById('uploadStatus').textContent = 'Folder upload complete!';
document.getElementById('uploadStatus').className = 'text-sm text-green-600 mt-2 font-medium';
// Final speed calculation
const elapsedMs = Date.now() - uploadStartTime;
const totalSize = getTotalSize(folderFiles);
const speed = calculateSpeed(totalSize, elapsedMs);
document.getElementById('uploadSpeed').textContent = `Average: ${speed}`;
showToast('Folder upload complete!', 'success');
isUploadingFolder = false;
setUploadingState(false);
return;
}
// Get next file
const file = folderFiles[currentFolderIndex];
// Get relative path within the folder
const relativePath = file.webkitRelativePath;
const pathParts = relativePath.split('/');
// Remove the first component (folder name) as we'll recreate it
pathParts.shift();
// Determine if there are subfolders in the path
const hasSubfolders = pathParts.length > 1;
const folderPath = hasSubfolders ? pathParts.slice(0, -1).join('/') : '';
const fileName = pathParts[pathParts.length - 1];
// Create the full path
let uploadPath = targetFolder;
if (uploadPath === '/') uploadPath = '';
// Add the folder name as the root folder
const fullPath = `${uploadPath}/${folderName}${folderPath ? '/' + folderPath : ''}`;
document.getElementById('uploadStatus').textContent = `Uploading file ${currentFolderIndex + 1}/${folderFiles.length}: ${relativePath}`;
// Update progress for the entire folder
const totalSize = getTotalSize(folderFiles);
const fileWeight = file.size / totalSize;
// Upload the file
if (file.size <= 10 * 1024 * 1024) { // 10MB limit for single chunk
uploadFolderFileSingleChunk(file, fullPath, fileName, fileWeight);
} else {
// For larger files, use multi-chunk upload
// This would need additional implementation to track folder progress too
uploadFolderFileMultiChunk(file, fullPath, fileName, fileWeight);
}
}
function uploadFolderFileSingleChunk(file, folderPath, fileName, fileWeight) {
const xhr = new XMLHttpRequest();
currentXhr = xhr; // Store reference for cancellation
xhr.open('POST', `/upload-chunk?path=${encodeURIComponent(folderPath)}`, true);
xhr.setRequestHeader('X-File-Name', encodeURIComponent(fileName));
xhr.setRequestHeader('X-Chunk-Index', '0');
xhr.setRequestHeader('X-Total-Chunks', '1');
xhr.setRequestHeader('X-Chunk-Size', file.size);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const pct = Math.floor((e.loaded / e.total) * 100);
// Calculate the contribution of this file to the overall folder progress
const fileProgress = (pct / 100) * fileWeight;
const baseProgress = folderUploadProgress;
const overallProgress = Math.floor(baseProgress + fileProgress * 100);
document.getElementById('uploadProgressBar').style.width = overallProgress + '%';
document.getElementById('uploadPercent').textContent = overallProgress + '%';
// Speed calculation for the entire folder upload
const elapsedMs = Date.now() - uploadStartTime;
const totalSizeUploaded = getTotalUploadedSize(currentFolderIndex, e.loaded);
const speed = calculateSpeed(totalSizeUploaded, elapsedMs);
document.getElementById('uploadSpeed').textContent = speed;
}
};
xhr.onload = () => {
if (cancelUploadRequested) {
return;
}
if (xhr.status === 200) {
try {
const resp = JSON.parse(xhr.responseText);
if (resp.success) {
// Update folder progress
folderUploadProgress += fileWeight * 100;
currentFolderIndex++;
// Continue with next file
uploadNextFolderFile(folderPath);
} else {
showToast(resp.error || 'Server error', 'error');
document.getElementById('uploadStatus').textContent = `Error uploading ${fileName}: ${resp.error || 'Server error'}`;
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
isUploadingFolder = false;
}
} catch {
showToast('Invalid response from server', 'error');
document.getElementById('uploadStatus').textContent = 'Invalid response from server';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
isUploadingFolder = false;
}
} else {
showToast(`Error: ${xhr.status}`, 'error');
document.getElementById('uploadStatus').textContent = `Error: ${xhr.status}`;
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
isUploadingFolder = false;
}
};
xhr.onerror = () => {
if (!cancelUploadRequested) {
showToast('Network error', 'error');
document.getElementById('uploadStatus').textContent = 'Network error';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
isUploadingFolder = false;
}
};
xhr.onabort = () => {
document.getElementById('uploadStatus').textContent = 'Upload aborted';
document.getElementById('uploadStatus').className = 'text-sm text-yellow-500 mt-2';
isUploadingFolder = false;
};
xhr.send(file);
}
function uploadFolderFileMultiChunk(file, folderPath, fileName, fileWeight) {
// Similar to uploadMultiChunk but adapted for folder upload
// This would need custom implementation
// For simplicity, we'll just use the single chunk method with a warning
document.getElementById('uploadStatus').textContent = `Warning: Large file (${formatSize(file.size)}) being uploaded as single chunk`;
uploadFolderFileSingleChunk(file, folderPath, fileName, fileWeight);
}
function getTotalUploadedSize(currentIndex, currentProgress) {
// Calculate total bytes uploaded so far
let totalUploaded = 0;
// Add size of all completed files
for (let i = 0; i < currentIndex; i++) {
totalUploaded += folderFiles[i].size;
}
// Add partial progress of current file
if (currentIndex < folderFiles.length) {
totalUploaded += currentProgress;
}
return totalUploaded;
}
// Single file upload functions
function uploadSingleChunk(file, folder) {
showToast(`Uploading ${file.name}`, 'info');
document.getElementById('uploadProgress').style.display = 'block';
document.getElementById('uploadProgressBar').style.width = '0%';
document.getElementById('uploadPercent').textContent = '0%';
document.getElementById('uploadSpeed').textContent = 'Calculating...';
document.getElementById('uploadStatus').textContent = 'Uploading...';
const xhr = new XMLHttpRequest();
currentXhr = xhr; // Store reference for cancellation
xhr.open('POST', `/upload-chunk?path=${encodeURIComponent(folder)}`, true);
xhr.setRequestHeader('X-File-Name', encodeURIComponent(file.name));
xhr.setRequestHeader('X-Chunk-Index', '0');
xhr.setRequestHeader('X-Total-Chunks', '1');
xhr.setRequestHeader('X-Chunk-Size', file.size);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const pct = Math.floor((e.loaded / e.total) * 100);
document.getElementById('uploadProgressBar').style.width = pct + '%';
document.getElementById('uploadPercent').textContent = pct + '%';
// Calculate and display speed
const elapsedMs = Date.now() - uploadStartTime;
const speed = calculateSpeed(e.loaded, elapsedMs);
document.getElementById('uploadSpeed').textContent = speed;
const remainingBytes = e.total - e.loaded;
if (e.loaded > 0 && elapsedMs > 1000) {
const bytesPerMs = e.loaded / elapsedMs;
const remainingTimeMs = bytesPerMs > 0 ? remainingBytes / bytesPerMs : 0;
const remainingTimeSec = Math.ceil(remainingTimeMs / 1000);
if (remainingTimeSec > 0) {
let timeDisplay = '';
if (remainingTimeSec > 60) {
const mins = Math.floor(remainingTimeSec / 60);
const secs = remainingTimeSec % 60;
timeDisplay = `${mins}m ${secs}s remaining`;
} else {
timeDisplay = `${remainingTimeSec}s remaining`;
}
document.getElementById('uploadStatus').textContent = `Uploading... ${timeDisplay}`;
}
}
}
};
xhr.onload = () => {
if (cancelUploadRequested) {
return; // Don't continue if cancellation was requested
}
if (xhr.status === 200) {
try {
const resp = JSON.parse(xhr.responseText);
if (resp.success) {
document.getElementById('uploadProgressBar').style.width = '100%';
document.getElementById('uploadPercent').textContent = '100%';
document.getElementById('uploadStatus').textContent = 'Upload complete!';
document.getElementById('uploadStatus').className = 'text-sm text-green-600 mt-2 font-medium';
// Final speed calculation
const elapsedMs = Date.now() - uploadStartTime;
const speed = calculateSpeed(file.size, elapsedMs);
document.getElementById('uploadSpeed').textContent = `Average: ${speed}`;
showToast('Upload complete!', 'success');
setUploadingState(false);
} else {
showToast(resp.error || 'Server error', 'error');
document.getElementById('uploadStatus').textContent = resp.error || 'Server error';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
} catch {
showToast('Invalid response from server', 'error');
document.getElementById('uploadStatus').textContent = 'Invalid response from server';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
} else {
showToast(`Error: ${xhr.status}`, 'error');
document.getElementById('uploadStatus').textContent = `Error: ${xhr.status}`;
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
};
xhr.onerror = () => {
if (!cancelUploadRequested) {
showToast('Network error', 'error');
document.getElementById('uploadStatus').textContent = 'Network error';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
};
xhr.onabort = () => {
document.getElementById('uploadStatus').textContent = 'Upload aborted';
document.getElementById('uploadStatus').className = 'text-sm text-yellow-500 mt-2';
};
xhr.send(file);
}
// MULTI-CHUNK
let chunkSize = 5 * 1024 * 1024;
let currentChunkIndex = 0;
let totalChunks = 0;
let fileId = null;
let totalBytesUploaded = 0;
function uploadMultiChunk(file, folder) {
// Setup UI
showToast(`Uploading ${file.name} in multiple chunks`, 'info');
document.getElementById('uploadProgress').style.display = 'block';
document.getElementById('uploadProgressBar').style.width = '0%';
document.getElementById('uploadPercent').textContent = '0%';
document.getElementById('uploadSpeed').textContent = 'Calculating...';
document.getElementById('uploadStatus').textContent = 'Preparing...';
// Setup state
chunkSize = 5 * 1024 * 1024; // 5MB chunks
totalChunks = Math.ceil(file.size / chunkSize);
currentChunkIndex = 0;
fileId = Date.now().toString(); // or any unique ID
totalBytesUploaded = 0;
// Start
uploadNextChunk(file, folder);
}
function uploadNextChunk(file, folder) {
if (cancelUploadRequested) {
return; // Don't continue if cancellation was requested
}
if (currentChunkIndex >= totalChunks) {
// done
showToast('All chunks sent!', 'info');
setUploadingState(false);
return;
}
const start = currentChunkIndex * chunkSize;
const end = Math.min(file.size, start + chunkSize);
const blob = file.slice(start, end);
const xhr = new XMLHttpRequest();
currentXhr = xhr; // Store reference for cancellation
xhr.open('POST', `/upload-chunk?path=${encodeURIComponent(folder)}`, true);
xhr.setRequestHeader('X-File-Name', encodeURIComponent(file.name));
xhr.setRequestHeader('X-Chunk-Index', currentChunkIndex);
xhr.setRequestHeader('X-Total-Chunks', totalChunks);
xhr.setRequestHeader('X-File-Id', fileId);
xhr.setRequestHeader('X-Chunk-Size', blob.size);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const chunkPercent = e.loaded / e.total;
const currentChunkUploaded = e.loaded;
const overallBytes = totalBytesUploaded + currentChunkUploaded;
const overallPercent = Math.floor((overallBytes / file.size) * 100);
document.getElementById('uploadProgressBar').style.width = overallPercent + '%';
document.getElementById('uploadPercent').textContent = overallPercent + '%';
// Calculate and display speed
const elapsedMs = Date.now() - uploadStartTime;
const speed = calculateSpeed(overallBytes, elapsedMs);
document.getElementById('uploadSpeed').textContent = speed;
// Estimate remaining time
if (overallBytes > 0 && elapsedMs > 1000) {
const bytesPerMs = overallBytes / elapsedMs;
const remainingBytes = file.size - overallBytes;
const remainingTimeMs = bytesPerMs > 0 ? remainingBytes / bytesPerMs : 0;
const remainingTimeSec = Math.ceil(remainingTimeMs / 1000);
let timeDisplay = '';
if (remainingTimeSec > 60) {
const mins = Math.floor(remainingTimeSec / 60);
const secs = remainingTimeSec % 60;
timeDisplay = `${mins}m ${secs}s remaining`;
} else {
timeDisplay = `${remainingTimeSec}s remaining`;
}
document.getElementById('uploadStatus').textContent =
`Uploading chunk ${currentChunkIndex+1} of ${totalChunks} - ${overallPercent}% overall - ${timeDisplay}`;
} else {
document.getElementById('uploadStatus').textContent =
`Uploading chunk ${currentChunkIndex+1} of ${totalChunks} - ${overallPercent}% overall`;
}
}
};
xhr.onload = () => {
if (cancelUploadRequested) {
return; // Don't continue if cancellation was requested
}
if (xhr.status === 200) {
try {
const resp = JSON.parse(xhr.responseText);
if (!resp.success) {
showToast(resp.error || 'Server error uploading chunk', 'error');
document.getElementById('uploadStatus').textContent = resp.error || 'Server error uploading chunk';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
return;
}
// Add current chunk size to total bytes uploaded
totalBytesUploaded += blob.size;
// success
if (currentChunkIndex === totalChunks - 1) {
// last chunk => server finalizes
document.getElementById('uploadProgressBar').style.width = '100%';
document.getElementById('uploadPercent').textContent = '100%';
document.getElementById('uploadStatus').textContent = 'Upload complete!';
document.getElementById('uploadStatus').className = 'text-sm text-green-600 mt-2 font-medium';
// Final speed calculation
const elapsedMs = Date.now() - uploadStartTime;
const speed = calculateSpeed(file.size, elapsedMs);
document.getElementById('uploadSpeed').textContent = `Average: ${speed}`;
showToast('Upload complete!', 'success');
setUploadingState(false);
} else {
currentChunkIndex++;
uploadNextChunk(file, folder);
}
} catch {
showToast('Invalid JSON response from server', 'error');
document.getElementById('uploadStatus').textContent = 'Invalid JSON response from server';
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
} else {
showToast(`Error uploading chunk ${currentChunkIndex}`, 'error');
document.getElementById('uploadStatus').textContent = `Error uploading chunk ${currentChunkIndex}`;
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
};
xhr.onerror = () => {
if (!cancelUploadRequested) {
showToast(`Network error uploading chunk ${currentChunkIndex}`, 'error');
document.getElementById('uploadStatus').textContent = `Network error uploading chunk ${currentChunkIndex}`;
document.getElementById('uploadStatus').className = 'text-sm text-red-500 mt-2';
setUploadingState(false);
}
};
xhr.onabort = () => {
document.getElementById('uploadStatus').textContent = 'Upload aborted';
document.getElementById('uploadStatus').className = 'text-sm text-yellow-500 mt-2';
};
xhr.send(blob);
}
// Drag drop
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.add('border-primary', 'bg-blue-50/30');
});
dropZone.addEventListener('dragleave', e => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('border-primary', 'bg-blue-50/30');
});
dropZone.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
dropZone.classList.remove('border-primary', 'bg-blue-50/30');
if (e.dataTransfer.files.length) {
document.getElementById('fileInput').files = e.dataTransfer.files;
onFileSelected();
}
});
// Toast
function showToast(msg, type='info', duration=4000) {
const toast = document.getElementById('toast');
const icon = toast.querySelector('i');
const text = document.getElementById('toastMsg');
text.textContent = msg;
toast.className = 'fixed bottom-4 left-4 flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg text-white opacity-0 transform translate-y-8 transition-all duration-300 z-50 max-w-md'; // reset
if (type === 'success') {
toast.classList.add('bg-green-500');
icon.className = 'fas fa-check-circle';
} else if (type === 'error') {
toast.classList.add('bg-red-500');
icon.className = 'fas fa-times-circle';
} else {
toast.classList.add('bg-primary');
icon.className = 'fas fa-info-circle';
}
toast.classList.add('opacity-100', 'translate-y-0');
setTimeout(() => {
toast.classList.remove('opacity-100', 'translate-y-0');
toast.classList.add('opacity-0', 'translate-y-8');
}, duration);
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
function showHelp() {
// Create a modern modal instead of using alert
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
modal.innerHTML = `
<div class="bg-white rounded-xl shadow-lg max-w-md w-full p-5 transform transition-all">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">How to Use</h3>
<button class="text-gray-400 hover:text-gray-500" id="closeHelpModal">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-3 text-gray-600">
<div class="flex items-start gap-3">
<div class="bg-primary/10 text-primary rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0 mt-0.5">1</div>
<p>Navigate to desired folder using the file explorer</p>
</div>
<div class="flex items-start gap-3">
<div class="bg-primary/10 text-primary rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0 mt-0.5">2</div>
<p>Click "Select This Folder" to choose the upload destination</p>
</div>
<div class="flex items-start gap-3">
<div class="bg-primary/10 text-primary rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0 mt-0.5">3</div>
<p>Click or drag & drop a file to the upload area</p>
</div>
<div class="flex items-start gap-3">
<div class="bg-primary/10 text-primary rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0 mt-0.5">4</div>
<p>Click the Upload button to start uploading</p>
</div>
</div>
<div class="mt-5 pt-3 border-t border-gray-100 text-xs text-gray-500">
Large files are automatically split into chunks to prevent corruption during upload.
</div>
</div>
`;
document.body.appendChild(modal);
document.getElementById('closeHelpModal').addEventListener('click', () => {
modal.classList.add('opacity-0');
setTimeout(() => {
document.body.removeChild(modal);
}, 300);
});
}
// Load files in the selected folder
function loadFiles(folderPath) {
const container = document.getElementById('fileContainer');
container.innerHTML = `
<div class="flex items-center justify-center h-[200px] text-gray-500">
<div class="flex items-center gap-2">
<i class="fas fa-spinner fa-spin text-primary"></i>
<span>Loading files...</span>
</div>
</div>`;
fetch(`/list-files?path=${encodeURIComponent(folderPath)}`)
.then(r => r.json())
.then(data => {
if (!data.success) {
container.innerHTML = `<div class="text-red-500 p-4">Error: ${data.error || 'Failed to load files'}</div>`;
return;
}
if (!data.files || data.files.length === 0) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center h-[200px] text-gray-500">
<i class="fas fa-file-alt text-gray-300 text-4xl mb-2"></i>
<span>No files in this folder</span>
</div>`;
// Update stats
document.getElementById('fileCount').textContent = '0 files';
document.getElementById('totalSize').textContent = '0 bytes';
return;
}
// Calculate total size and update stats
let totalSize = 0;
data.files.forEach(file => {
totalSize += file.size || 0;
});
document.getElementById('fileCount').textContent = `${data.files.length} file${data.files.length !== 1 ? 's' : ''}`;
document.getElementById('totalSize').textContent = formatSize(totalSize);
// Clear selected files
selectedFiles.clear();
updateDownloadButton();
// Create grid view for files
container.innerHTML = `
<div class="flex justify-between items-center border-b border-gray-100 pb-3 mb-3">
<div class="flex items-center gap-2">
<input type="checkbox" id="selectAllFiles" class="rounded border-gray-300" onchange="toggleSelectAll(this.checked)">
<label for="selectAllFiles" class="text-sm text-gray-700 cursor-pointer">Select All</label>
</div>
<div class="text-xs text-gray-500">
<button onclick="toggleViewMode()" class="p-1 rounded-md hover:bg-gray-100 flex items-center gap-1">
<i id="viewModeIcon" class="fas fa-th-large"></i>
<span id="viewModeText">Grid View</span>
</button>
</div>
</div>
<div id="fileGrid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3"></div>
<div id="fileList" class="hidden"></div>
`;
const fileGrid = document.getElementById('fileGrid');
const fileList = document.getElementById('fileList');
// Populate grid view
data.files.forEach((file, idx) => {
const fileCard = document.createElement('div');
fileCard.className = 'bg-white border border-gray-200 rounded-lg p-3 hover:shadow-sm transition-all';
const icon = getFileIcon(file.type, file.extension);
fileCard.innerHTML = `
<div class="flex items-start">
<div class="flex-shrink-0 p-2 rounded-lg ${getColorForFileType(file.type)} mb-2 mr-2">
<i class="${icon} text-white"></i>
</div>
<div class="flex-grow min-w-0">
<div class="flex items-start justify-between">
<div class="truncate max-w-[140px]" title="${file.name}">
<span class="font-medium text-gray-700">${file.name}</span>
</div>
<div class="ml-2 flex-shrink-0">
<input type="checkbox" id="file_grid_${idx}" data-file="${encodeURIComponent(folderPath + '/' + file.name)}"
class="file-checkbox rounded border-gray-300" onchange="toggleFileSelection(this)">
</div>
</div>
<div class="text-xs text-gray-500 mt-1">${formatSize(file.size)}</div>
<div class="flex items-center justify-between mt-2">
<span class="text-xs text-gray-400">${formatDate(file.modified)}</span>
<button onclick="downloadSingleFile('${encodeURIComponent(folderPath + '/' + file.name)}')"
class="bg-primary hover:bg-primary-dark text-white p-1 rounded-md transition-colors">
<i class="fas fa-download text-xs"></i>
</button>
</div>
</div>
</div>
`;
fileGrid.appendChild(fileCard);
});
// Populate list view (hidden by default)
fileList.innerHTML = `
<table class="w-full">
<thead>
<tr class="bg-gray-50 text-left text-gray-500 text-xs">
<th class="px-3 py-2 w-8"></th>
<th class="px-3 py-2">Name</th>
<th class="px-3 py-2 w-28">Size</th>
<th class="px-3 py-2 w-40">Modified</th>
<th class="px-3 py-2 w-20"></th>
</tr>
</thead>
<tbody>
${data.files.map((file, idx) => `
<tr class="${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'} border-t border-gray-100 hover:bg-blue-50/30">
<td class="px-3 py-2 w-8">
<input type="checkbox" id="file_list_${idx}" data-file="${encodeURIComponent(folderPath + '/' + file.name)}"
class="file-checkbox rounded border-gray-300" onchange="toggleFileSelection(this)">
</td>
<td class="px-3 py-2 truncate max-w-xs">
<div class="flex items-center gap-2">
<i class="${getFileIcon(file.type, file.extension)} ${getTextColorForFileType(file.type)}"></i>
<span class="truncate">${file.name}</span>
</div>
</td>
<td class="px-3 py-2 text-gray-500 text-sm">${formatSize(file.size)}</td>
<td class="px-3 py-2 text-gray-500 text-sm">${formatDate(file.modified)}</td>
<td class="px-3 py-2">
<button onclick="downloadSingleFile('${encodeURIComponent(folderPath + '/' + file.name)}')"
class="bg-primary hover:bg-primary-dark text-white p-1.5 rounded transition-colors">
<i class="fas fa-download"></i>
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
// Sync checkboxes between grid and list views
syncCheckboxes();
})
.catch(err => {
container.innerHTML = `<div class="text-red-500 p-4">Failed to load files: ${err}</div>`;
});
}
// Toggle between grid and list view
let currentView = 'grid'; // Default view
function toggleViewMode() {
const gridView = document.getElementById('fileGrid');
const listView = document.getElementById('fileList');
const icon = document.getElementById('viewModeIcon');
const text = document.getElementById('viewModeText');
if (currentView === 'grid') {
gridView.classList.add('hidden');
listView.classList.remove('hidden');
icon.className = 'fas fa-list';
text.textContent = 'List View';
currentView = 'list';
} else {
gridView.classList.remove('hidden');
listView.classList.add('hidden');
icon.className = 'fas fa-th-large';
text.textContent = 'Grid View';
currentView = 'grid';
}
// Make sure all checkboxes are in sync
syncCheckboxes();
}
// Function to sync checkbox states between grid and list views
function syncCheckboxes() {
// Get all file paths that should be checked
const checkedPaths = Array.from(selectedFiles);
// Uncheck all boxes first
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.checked = checkedPaths.includes(cb.dataset.file);
});
// Check the "select all" box if all files are selected
const selectAllCheckbox = document.getElementById('selectAllFiles');
const totalCheckboxes = document.querySelectorAll('.file-checkbox').length;
selectAllCheckbox.checked = (totalCheckboxes > 0 && selectedFiles.size === totalCheckboxes);
}
// Get appropriate icon for file type
function getFileIcon(type, extension) {
switch (type) {
case 'image':
return 'fas fa-file-image';
case 'document':
if (extension === 'pdf') return 'fas fa-file-pdf';
if (['doc', 'docx'].includes(extension)) return 'fas fa-file-word';
if (['xls', 'xlsx', 'csv'].includes(extension)) return 'fas fa-file-excel';
if (['ppt', 'pptx'].includes(extension)) return 'fas fa-file-powerpoint';
return 'fas fa-file-alt';
case 'archive':
return 'fas fa-file-archive';
case 'audio':
return 'fas fa-file-audio';
case 'video':
return 'fas fa-file-video';
case 'code':
return 'fas fa-file-code';
default:
return 'fas fa-file';
}
}
// Get background color for file type icon
function getColorForFileType(type) {
switch (type) {
case 'image':
return 'bg-purple-500';
case 'document':
return 'bg-blue-500';
case 'archive':
return 'bg-yellow-500';
case 'audio':
return 'bg-green-500';
case 'video':
return 'bg-red-500';
case 'code':
return 'bg-gray-700';
default:
return 'bg-gray-500';
}
}
// Get text color for file type icon in list view
function getTextColorForFileType(type) {
switch (type) {
case 'image':
return 'text-purple-500';
case 'document':
return 'text-blue-500';
case 'archive':
return 'text-yellow-500';
case 'audio':
return 'text-green-500';
case 'video':
return 'text-red-500';
case 'code':
return 'text-gray-700';
default:
return 'text-gray-500';
}
}
// Format date for display
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
// If today, show time
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// If this year, show month and day
if (date.getFullYear() === now.getFullYear()) {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
// Otherwise show full date
return date.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' });
}
// Toggle selection of all files
function toggleSelectAll(checked) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => {
cb.checked = checked;
toggleFileSelection(cb, true); // Skip individual updates
});
updateDownloadButton();
}
// Toggle selection of a single file
function toggleFileSelection(checkbox, skipUpdate) {
const filePath = checkbox.dataset.file;
if (checkbox.checked) {
selectedFiles.add(filePath);
} else {
selectedFiles.delete(filePath);
}
// Sync the corresponding checkbox in the other view
const isGridCheckbox = checkbox.id.startsWith('file_grid_');
const idx = checkbox.id.split('_').pop();
if (isGridCheckbox) {
const listCheckbox = document.getElementById(`file_list_${idx}`);
if (listCheckbox) listCheckbox.checked = checkbox.checked;
} else {
const gridCheckbox = document.getElementById(`file_grid_${idx}`);
if (gridCheckbox) gridCheckbox.checked = checkbox.checked;
}
if (!skipUpdate) {
updateDownloadButton();
// Update "select all" checkbox
const selectAllCheckbox = document.getElementById('selectAllFiles');
const totalCheckboxes = document.querySelectorAll('.file-checkbox').length;
selectAllCheckbox.checked = (totalCheckboxes > 0 && selectedFiles.size === totalCheckboxes);
}
}
// Update the download button based on selection state
function updateDownloadButton() {
const downloadBtn = document.getElementById('downloadSelectedBtn');
if (selectedFiles.size > 0) {
downloadBtn.disabled = false;
downloadBtn.classList.remove('opacity-50', 'cursor-not-allowed');
downloadBtn.querySelector('span').textContent = `Download (${selectedFiles.size})`;
} else {
downloadBtn.disabled = true;
downloadBtn.classList.add('opacity-50', 'cursor-not-allowed');
downloadBtn.querySelector('span').textContent = 'Download Selected';
}
}
// Download a single file
function downloadSingleFile(filePath) {
// Using a direct window.location approach for simple downloads
window.location.href = `/download?file=${filePath}`;
}
// Download multiple selected files
function downloadSelected() {
if (selectedFiles.size === 0) {
showToast('No files selected', 'error');
return;
}
if (selectedFiles.size === 1) {
// If only one file is selected, use the single file download
downloadSingleFile(Array.from(selectedFiles)[0]);
return;
}
// For multiple files, construct the URL with all selected files
const filesParam = Array.from(selectedFiles).join(',');
window.location.href = `/download-multiple?files=${filesParam}`;
}
// Refresh file list
function refreshFiles() {
loadFiles(currentPath);
}
</script>
</body>
</html>