// fileManager.js import { escapeHTML, updateFileActionButtons, showToast } from './domUtils.js'; import { formatFolderName } from './folderManager.js'; export let fileData = []; export let sortOrder = { column: "uploaded", ascending: true }; // Global pagination defaults window.itemsPerPage = window.itemsPerPage || 10; window.currentPage = window.currentPage || 1; // ------------------------------- // Helper Functions // ------------------------------- // Parse date strings in "m/d/y h:iA" format into a timestamp. function parseCustomDate(dateStr) { dateStr = dateStr.replace(/\s+/g, " ").trim(); const parts = dateStr.split(" "); if (parts.length !== 2) { return new Date(dateStr).getTime(); } const datePart = parts[0]; const timePart = parts[1]; const dateComponents = datePart.split("/"); if (dateComponents.length !== 3) { return new Date(dateStr).getTime(); } let month = parseInt(dateComponents[0], 10); let day = parseInt(dateComponents[1], 10); let year = parseInt(dateComponents[2], 10); if (year < 100) { year += 2000; } const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i; const match = timePart.match(timeRegex); if (!match) { return new Date(dateStr).getTime(); } let hour = parseInt(match[1], 10); const minute = parseInt(match[2], 10); const period = match[3].toUpperCase(); if (period === "PM" && hour !== 12) { hour += 12; } if (period === "AM" && hour === 12) { hour = 0; } return new Date(year, month - 1, day, hour, minute).getTime(); } // Determines if a file is editable based on its extension. export function canEditFile(fileName) { const allowedExtensions = [ "txt", "html", "htm", "css", "js", "json", "xml", "md", "py", "ini", "csv", "log", "conf", "config", "bat", "rtf", "doc", "docx" ]; const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); return allowedExtensions.includes(ext); } // ------------------------------- // Global Functions (attached to window) // ------------------------------- window.toggleRowSelection = function (event, fileName) { const targetTag = event.target.tagName.toLowerCase(); if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') { return; } const row = event.currentTarget; const checkbox = row.querySelector('.file-checkbox'); if (!checkbox) return; checkbox.checked = !checkbox.checked; window.updateRowHighlight(checkbox); updateFileActionButtons(); }; window.updateRowHighlight = function (checkbox) { const row = checkbox.closest('tr'); if (!row) return; if (checkbox.checked) { row.classList.add('row-selected'); } else { row.classList.remove('row-selected'); } }; // ------------------------------- // File List Rendering // ------------------------------- export function loadFileList(folderParam) { const folder = folderParam || "root"; // Request a recursive listing from the server. return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) .then(response => response.json()) .then(data => { const fileListContainer = document.getElementById("fileList"); fileListContainer.innerHTML = ""; if (data.files && data.files.length > 0) { // Map each file so that we have a full name that includes subfolder information. // We assume that getFileList.php returns a property 'path' that contains the full relative path (e.g. "subfolder/filename.txt") data.files = data.files.map(file => { // If file.path exists, use that; otherwise fallback to file.name. file.fullName = (file.path || file.name).trim().toLowerCase(); return file; }); // Save fileData and render file table using your full list. fileData = data.files; renderFileTable(folder); } else { fileListContainer.textContent = "No files found."; updateFileActionButtons(); } // Return the full file objects. return data.files || []; }) .catch(error => { console.error("Error loading file list:", error); return []; }); } // Debounce helper (if not defined already) function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } export function renderFileTable(folder) { const fileListContainer = document.getElementById("fileList"); const folderPath = (folder === "root") ? "uploads/" : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; // Use the global search term if available. const searchTerm = window.currentSearchTerm || ""; const filteredFiles = fileData.filter(file => file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); // Get persistent items per page from localStorage. const itemsPerPageSetting = parseInt(localStorage.getItem('itemsPerPage') || '10', 10); // Use a mutable currentPage variable let currentPage = window.currentPage || 1; const totalFiles = filteredFiles.length; const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); // If the current page is greater than totalPages, reset it to a valid page (for example, page 1 or totalPages) if (currentPage > totalPages) { currentPage = totalPages > 0 ? totalPages : 1; window.currentPage = currentPage; } const safeSearchTerm = escapeHTML(searchTerm); const topControlsHTML = `
search
Page ${currentPage} of ${totalPages || 1}
`; let tableHTML = ` `; const startIndex = (currentPage - 1) * itemsPerPageSetting; const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); let tableBody = ``; if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach(file => { const isEditable = canEditFile(file.name); const safeFileName = escapeHTML(file.name); const safeModified = escapeHTML(file.modified); const safeUploaded = escapeHTML(file.uploaded); const safeSize = escapeHTML(file.size); const safeUploader = escapeHTML(file.uploader || "Unknown"); const isViewable = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|ogg)$/i.test(file.name); let previewButton = ""; if (isViewable) { let previewIcon = ""; if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) { previewIcon = `image`; } else if (/\.(mp4|webm|mov|ogg)$/i.test(file.name)) { previewIcon = `videocam`; } else if (/\.pdf$/i.test(file.name)) { previewIcon = `picture_as_pdf`; } previewButton = ``; } tableBody += ` `; }); } else { tableBody += ``; } tableBody += `
File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""} Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""} Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""} File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""} Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""} Actions
${safeFileName} ${safeModified} ${safeUploaded} ${safeSize} ${safeUploader}
Download ${isEditable ? `` : ""} ${previewButton}
No files found.
`; const bottomControlsHTML = `
items per page
`; fileListContainer.innerHTML = topControlsHTML + tableHTML + tableBody + bottomControlsHTML; // Re-attach event listener for the new search input element. const newSearchInput = document.getElementById("searchInput"); if (newSearchInput) { newSearchInput.addEventListener("input", debounce(function () { window.currentSearchTerm = newSearchInput.value; window.currentPage = 1; renderFileTable(folder); // After re-rendering, restore focus and caret position. setTimeout(() => { const freshInput = document.getElementById("searchInput"); if (freshInput) { freshInput.focus(); freshInput.setSelectionRange(freshInput.value.length, freshInput.value.length); } }, 0); }, 300)); } // Add event listeners for header sorting. document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { cell.addEventListener("click", function () { const column = this.getAttribute("data-column"); sortFiles(column, folder); }); }); // Add event listeners for checkboxes. document.querySelectorAll('#fileList .file-checkbox').forEach(checkbox => { checkbox.addEventListener('change', function (e) { updateRowHighlight(e.target); updateFileActionButtons(); }); }); updateFileActionButtons(); } // Global function to show an image preview modal. window.previewFile = function (fileUrl, fileName) { let modal = document.getElementById("filePreviewModal"); if (!modal) { modal = document.createElement("div"); modal.id = "filePreviewModal"; // Use the same styling as the original image modal. Object.assign(modal.style, { display: "none", position: "fixed", top: "0", left: "0", width: "100vw", height: "100vh", backgroundColor: "rgba(0,0,0,0.7)", display: "flex", justifyContent: "center", alignItems: "center", zIndex: "1000" }); modal.innerHTML = ` `; document.body.appendChild(modal); document.getElementById("closeFileModal").addEventListener("click", function () { modal.style.display = "none"; }); modal.addEventListener("click", function (e) { if (e.target === modal) { modal.style.display = "none"; } }); } modal.querySelector("h4").textContent = "Preview: " + fileName; const container = modal.querySelector(".file-preview-container"); container.innerHTML = ""; // Clear previous content const extension = fileName.split('.').pop().toLowerCase(); if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) { // Image preview const img = document.createElement("img"); img.src = fileUrl; img.className = "image-modal-img"; container.appendChild(img); } else if (extension === "pdf") { // PDF preview using with explicit sizing const embed = document.createElement("embed"); // Append a timestamp to force reload const separator = fileUrl.indexOf('?') === -1 ? '?' : '&'; embed.src = fileUrl + separator + 't=' + new Date().getTime(); embed.type = "application/pdf"; embed.style.width = "80vw"; embed.style.height = "80vh"; embed.style.border = "none"; container.appendChild(embed); } else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) { // Video preview using