// State let currentPath = ''; let currentUser = null; let selectedFiles = new Set(); let filesData = []; let viewMode = localStorage.getItem('viewMode') || 'list'; // 'list' or 'grid' // Elements const loginScreen = document.getElementById('login-screen'); const appScreen = document.getElementById('app-screen'); const loginForm = document.getElementById('login-form'); const loginError = document.getElementById('login-error'); const fileList = document.getElementById('file-list'); const currentPathDisplay = document.getElementById('current-path-display'); const upDirBtn = document.getElementById('up-dir-btn'); const bulkDownloadBtn = document.getElementById('bulk-download-btn'); const viewToggleBtn = document.getElementById('view-toggle-btn'); const selectAllCheckbox = document.getElementById('select-all'); const dropZone = document.getElementById('drop-zone'); // Init checkAuth(); // --- Auth --- async function checkAuth() { try { const res = await fetch('/api/auth/me'); const data = await res.json(); if (data.authenticated) { currentUser = data.user; showApp(); } else { showLogin(); } } catch (e) { showLogin(); } } loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); try { const res = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await res.json(); if (result.success) { currentUser = result.user; showApp(); } else { loginError.textContent = result.error; } } catch (e) { loginError.textContent = 'Login failed'; } }); document.getElementById('logout-btn').addEventListener('click', async () => { await fetch('/api/auth/logout', { method: 'POST' }); window.location.reload(); }); // --- View Mode --- function toggleView() { viewMode = viewMode === 'list' ? 'grid' : 'list'; localStorage.setItem('viewMode', viewMode); updateViewIcon(); renderFiles(filesData); } function updateViewIcon() { viewToggleBtn.textContent = viewMode === 'list' ? '📅' : '📄'; const filesSection = document.querySelector('.files-section'); if (viewMode === 'grid') filesSection.classList.add('grid-view'); else filesSection.classList.remove('grid-view'); } viewToggleBtn.addEventListener('click', toggleView); // --- Auth / UI State --- function showLogin() { // Force style to ensure hidden/visible loginScreen.classList.remove('hidden'); loginScreen.style.display = 'flex'; appScreen.classList.add('hidden'); } function showApp() { loginScreen.classList.add('hidden'); loginScreen.style.display = 'none'; // Force hide appScreen.classList.remove('hidden'); document.getElementById('user-display').textContent = currentUser.username; loadFiles(); updateViewIcon(); } // --- Files --- async function loadFiles(path = currentPath) { try { const res = await fetch(`/api/files?path=${encodeURIComponent(path)}`); if (res.status === 401) return window.location.reload(); const files = await res.json(); filesData = files; currentPath = path; updateBreadcrumbs(); renderFiles(files); selectedFiles.clear(); updateToolbar(); } catch (e) { console.error("Load failed", e); } } function renderFiles(files) { fileList.innerHTML = ''; // Parent Directory Item if (currentPath) { const li = document.createElement('li'); li.className = 'file-item'; li.dataset.name = '..'; li.dataset.type = 'folder'; const icon = '🔙'; li.innerHTML = ` ${icon} .. - - `; li.addEventListener('click', () => { const parts = currentPath.split('/'); parts.pop(); loadFiles(parts.join('/')); }); li.addEventListener('dragover', handleDragOverFolder); li.addEventListener('drop', handleDropOnParent); li.addEventListener('dragleave', handleDragLeaveFolder); fileList.appendChild(li); } if (files.length === 0 && !currentPath) { fileList.innerHTML = '
Empty directory
'; return; } files.forEach(file => { const li = document.createElement('li'); li.className = 'file-item'; li.draggable = true; li.dataset.name = file.name; li.dataset.type = file.isDirectory ? 'folder' : 'file'; const icon = file.isDirectory ? '📁' : getFileIcon(file.name); const size = file.isDirectory ? '-' : formatSize(file.size); const date = new Date(file.mtime).toLocaleDateString(); li.innerHTML = ` ${icon} ${file.name} ${size} ${date} `; const nameEl = li.querySelector('.file-name'); const openItem = () => { if (file.isDirectory) { loadFiles(currentPath ? currentPath + '/' + file.name : file.name); } else { previewOrDownload(file); } }; nameEl.addEventListener('click', openItem); // Also click icon in grid view li.querySelector('.col-icon').addEventListener('click', () => { if (viewMode === 'grid') openItem(); }); // Drag events li.addEventListener('dragstart', handleDragStart); if (file.isDirectory) { li.addEventListener('dragover', handleDragOverFolder); li.addEventListener('drop', handleDropOnFolder); li.addEventListener('dragleave', handleDragLeaveFolder); } // Checkbox event const checkbox = li.querySelector('.file-select'); checkbox.addEventListener('change', (e) => { if (e.target.checked) selectedFiles.add(file.name); else selectedFiles.delete(file.name); updateToolbar(); }); fileList.appendChild(li); }); } function updateBreadcrumbs() { currentPathDisplay.textContent = currentPath ? '/' + currentPath : '/'; upDirBtn.disabled = !currentPath; } upDirBtn.addEventListener('click', () => { if (!currentPath) return; const parts = currentPath.split('/'); parts.pop(); loadFiles(parts.join('/')); }); document.getElementById('refresh-btn').addEventListener('click', () => loadFiles()); // --- File Operations --- function getFileIcon(filename) { const ext = filename.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) return '🖼️'; if (['mp4', 'webm', 'mov'].includes(ext)) return '🎥'; if (['mp3', 'wav'].includes(ext)) return '🎵'; if (['zip', 'rar', '7z', 'tar'].includes(ext)) return '📦'; if (['pdf'].includes(ext)) return '📄'; return '📄'; } function previewOrDownload(file) { const ext = file.name.split('.').pop().toLowerCase(); const filePath = currentPath ? currentPath + '/' + file.name : file.name; const encodedPath = encodeURIComponent(filePath); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) { openMediaModal(`${file.name}`); } else if (['mp4', 'webm', 'mov'].includes(ext)) { openMediaModal(``); } else if (['zip'].includes(ext)) { openZipModal(filePath); } else { window.open(`/api/download?path=${encodedPath}`, '_blank'); } } // Media Modal const mediaModal = document.getElementById('media-modal'); const mediaContainer = document.getElementById('media-container'); function openMediaModal(html) { mediaContainer.innerHTML = html; mediaModal.classList.remove('hidden'); } // ZIP Modal const zipModal = document.getElementById('zip-modal'); const zipList = document.getElementById('zip-list'); async function openZipModal(filePath) { try { const res = await fetch(`/api/zip/preview?path=${encodeURIComponent(filePath)}`); if (!res.ok) throw new Error('Failed'); const entries = await res.json(); zipList.innerHTML = ''; entries.forEach(entry => { const li = document.createElement('li'); li.textContent = `${entry.isDirectory ? '📁' : '📄'} ${entry.name} (${formatSize(entry.size)})`; zipList.appendChild(li); }); zipModal.classList.remove('hidden'); } catch (e) { alert("Could not preview ZIP"); } } // Close Modals on backdrop click [mediaModal, zipModal].forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.add('hidden'); if (modal === mediaModal) mediaContainer.innerHTML = ''; } }); const closeBtn = modal.querySelector('.close-modal'); if (closeBtn) { closeBtn.addEventListener('click', () => { modal.classList.add('hidden'); if (modal === mediaModal) mediaContainer.innerHTML = ''; }); } }); // Bulk Actions selectAllCheckbox.addEventListener('change', (e) => { const checkboxes = document.querySelectorAll('.file-select'); checkboxes.forEach(cb => { cb.checked = e.target.checked; if (e.target.checked) selectedFiles.add(cb.value); else selectedFiles.delete(cb.value); }); updateToolbar(); }); function updateToolbar() { if (selectedFiles.size > 0) { bulkDownloadBtn.classList.remove('hidden'); bulkDownloadBtn.textContent = `Download (${selectedFiles.size})`; } else { bulkDownloadBtn.classList.add('hidden'); } } bulkDownloadBtn.addEventListener('click', async () => { const paths = Array.from(selectedFiles).map(name => currentPath ? currentPath + '/' + name : name); try { const res = await fetch('/api/bulk-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }) }); if (res.ok) { const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'files.zip'; document.body.appendChild(a); a.click(); a.remove(); } } catch (e) { console.error(e); } }); // New Folder document.getElementById('new-folder-btn').addEventListener('click', async () => { const name = prompt("Folder name:"); if (!name) return; await fetch('/api/folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, path: currentPath }) }); loadFiles(); }); // Delete async function deleteItem(name) { if (!confirm(`Delete ${name}?`)) return; const itemPath = currentPath ? currentPath + '/' + name : name; await fetch(`/api/files?path=${encodeURIComponent(itemPath)}`, { method: 'DELETE' }); loadFiles(); } // Drag & Drop Upload dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { uploadFiles(e.dataTransfer.files); } }); async function uploadFiles(files) { const statusDiv = document.getElementById('upload-status'); statusDiv.innerHTML = 'Uploading...'; for (let file of files) { const formData = new FormData(); formData.append('file', file); if (currentPath) { await fetch(`/api/upload?path=${encodeURIComponent(currentPath)}`, { method: 'POST', body: formData }); } else { await fetch('/api/upload', { method: 'POST', body: formData }); } } statusDiv.innerHTML = 'Done!'; setTimeout(() => statusDiv.innerHTML = '', 2000); loadFiles(); } // Drag & Drop Move (File to Folder) let draggedItem = null; function handleDragStart(e) { draggedItem = e.target.dataset.name; } function handleDragOverFolder(e) { e.preventDefault(); e.currentTarget.style.backgroundColor = 'rgba(59, 130, 246, 0.1)'; } function handleDragLeaveFolder(e) { e.currentTarget.style.backgroundColor = ''; } async function handleDropOnFolder(e) { e.preventDefault(); e.currentTarget.style.backgroundColor = ''; const targetFolder = e.currentTarget.dataset.name; if (draggedItem === targetFolder) return; if (confirm(`Move ${draggedItem} to ${targetFolder}?`)) { const source = currentPath ? currentPath + '/' + draggedItem : draggedItem; const dest = currentPath ? currentPath + '/' + targetFolder + '/' + draggedItem : targetFolder + '/' + draggedItem; await performMove(source, dest); } } async function handleDropOnParent(e) { e.preventDefault(); e.currentTarget.style.backgroundColor = ''; if (!currentPath) return; if (confirm(`Move ${draggedItem} up one level?`)) { const source = currentPath + '/' + draggedItem; const parts = currentPath.split('/'); parts.pop(); const parentPath = parts.join('/'); const dest = parentPath ? parentPath + '/' + draggedItem : draggedItem; await performMove(source, dest); } } async function performMove(source, dest) { await fetch('/api/ops/move', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source, destination: dest }) }); loadFiles(); } // Helpers function formatSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }