Files
FTP-Server/public/app.js
2026-02-09 16:27:58 +01:00

541 lines
17 KiB
JavaScript

// 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 = `
<span class="col-select" style="visibility:hidden"></span>
<span class="col-icon">${icon}</span>
<span class="file-name is-folder col-name">..</span>
<span class="col-size">-</span>
<span class="col-date">-</span>
<span class="col-actions"></span>
`;
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 = '<div style="padding:1rem; text-align:center; color:var(--text-secondary)">Empty directory</div>';
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 = `
<span class="col-select"><input type="checkbox" class="file-select" value="${file.name}"></span>
<span class="col-icon">${icon}</span>
<span class="file-name ${file.isDirectory ? 'is-folder' : ''} col-name" title="${file.name}">${file.name}</span>
<span class="col-size">${size}</span>
<span class="col-date">${date}</span>
<span class="col-actions">
<button class="btn danger" onclick="deleteItem('${file.name}')" title="Delete">🗑️</button>
</span>
`;
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(`<img src="/api/download?path=${encodedPath}" alt="${file.name}">`);
} else if (['mp4', 'webm', 'mov'].includes(ext)) {
openMediaModal(`<video controls src="/api/download?path=${encodedPath}"></video>`);
} 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);
}
});
// File Input Change
document.getElementById('file-input').addEventListener('change', (e) => {
if (e.target.files.length > 0) {
uploadFiles(e.target.files);
// Reset input so same file can be selected again if needed
e.target.value = '';
}
});
async function uploadFiles(files) {
const statusDiv = document.getElementById('upload-status');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
statusDiv.innerHTML = 'Uploading...';
progressContainer.classList.remove('hidden');
progressBar.style.width = '0%';
progressText.textContent = '0%';
let totalSize = 0;
for (let file of files) totalSize += file.size;
// Avoid division by zero
if (totalSize === 0) totalSize = 1;
let previousFilesSize = 0;
for (let file of files) {
try {
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
const url = currentPath
? `/api/upload?path=${encodeURIComponent(currentPath)}`
: '/api/upload';
xhr.open('POST', url);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const currentLoaded = e.loaded;
const totalLoaded = previousFilesSize + currentLoaded;
const percent = Math.min(100, Math.round((totalLoaded / totalSize) * 100));
progressBar.style.width = percent + '%';
progressText.textContent = percent + '%';
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send(formData);
});
previousFilesSize += file.size;
} catch (e) {
console.error(e);
statusDiv.innerHTML = `<span style="color:var(--danger-color)">Error uploading ${file.name}</span>`;
}
}
progressBar.style.width = '100%';
progressText.textContent = '100%';
statusDiv.innerHTML = 'Done!';
setTimeout(() => {
statusDiv.innerHTML = '';
progressContainer.classList.add('hidden');
progressBar.style.width = '0%';
progressText.textContent = '0%';
}, 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];
}