From 899b04e49a14cc077199560b066c2fd851db6265 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 5 Apr 2025 15:14:49 -0400 Subject: [PATCH] Modularize fileManager.js --- .htaccess | 48 ++ js/auth.js | 4 +- js/fileActions.js | 476 +++++++++++ js/fileDragDrop.js | 125 +++ js/fileEditor.js | 178 ++++ js/fileListView.js | 406 ++++++++++ js/fileManager.js | 1652 +------------------------------------- js/fileMenu.js | 155 ++++ js/filePreview.js | 256 ++++++ js/folderManager.js | 2 +- js/main.js | 19 +- js/trashRestoreDelete.js | 2 +- js/upload.js | 4 +- 13 files changed, 1677 insertions(+), 1650 deletions(-) create mode 100644 .htaccess create mode 100644 js/fileActions.js create mode 100644 js/fileDragDrop.js create mode 100644 js/fileEditor.js create mode 100644 js/fileListView.js create mode 100644 js/fileMenu.js create mode 100644 js/filePreview.js diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..a9531d2 --- /dev/null +++ b/.htaccess @@ -0,0 +1,48 @@ +# ----------------------------- +# 1) Prevent directory listings +# ----------------------------- +Options -Indexes + +# ----------------------------- +# 2) Default index files +# ----------------------------- +DirectoryIndex index.html + +# ----------------------------- +# 3) Deny access to hidden files +# ----------------------------- +# (blocks access to .htaccess, .gitignore, etc.) + + Require all denied + + +# ----------------------------- +# 4) Enforce HTTPS (optional) +# ----------------------------- +# Uncomment if you have SSL configured +#RewriteEngine On +#RewriteCond %{HTTPS} off +#RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + + # Prevent clickjacking + Header always set X-Frame-Options "SAMEORIGIN" + # Block XSS + Header always set X-XSS-Protection "1; mode=block" + # No MIME sniffing + Header always set X-Content-Type-Options "nosniff" + + + + # HTML: always revalidate + + Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header set Expires "0" + + + # JS/CSS: short‑term cache, revalidate regularly + + Header set Cache-Control "public, max-age=3600, must-revalidate" + + \ No newline at end of file diff --git a/js/auth.js b/js/auth.js index 3aebecf..4dfb593 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,6 +1,8 @@ import { sendRequest } from './networkUtils.js'; import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js'; -import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js'; +import { loadFileList } from './fileListView.js'; +import { initFileActions } from './fileActions.js'; +import { renderFileTable } from './fileListView.js'; import { loadFolderTree } from './folderManager.js'; import { openTOTPLoginModal, diff --git a/js/fileActions.js b/js/fileActions.js new file mode 100644 index 0000000..7e27b74 --- /dev/null +++ b/js/fileActions.js @@ -0,0 +1,476 @@ +// fileActions.js +import { showToast, attachEnterKeyListener } from './domUtils.js'; +import { loadFileList } from './fileListView.js'; +import { formatFolderName } from './fileListView.js'; + +export function handleDeleteSelected(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (checkboxes.length === 0) { + showToast("No files selected."); + return; + } + window.filesToDelete = Array.from(checkboxes).map(chk => chk.value); + document.getElementById("deleteFilesMessage").textContent = + "Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?"; + document.getElementById("deleteFilesModal").style.display = "block"; + attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles"); +} + +document.addEventListener("DOMContentLoaded", function () { + const cancelDelete = document.getElementById("cancelDeleteFiles"); + if (cancelDelete) { + cancelDelete.addEventListener("click", function () { + document.getElementById("deleteFilesModal").style.display = "none"; + window.filesToDelete = []; + }); + } + + const confirmDelete = document.getElementById("confirmDeleteFiles"); + if (confirmDelete) { + confirmDelete.addEventListener("click", function () { + fetch("deleteFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files deleted successfully!"); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not delete files")); + } + }) + .catch(error => console.error("Error deleting files:", error)) + .finally(() => { + document.getElementById("deleteFilesModal").style.display = "none"; + window.filesToDelete = []; + }); + }); + } +}); + +attachEnterKeyListener("downloadZipModal", "confirmDownloadZip"); +export function handleDownloadZipSelected(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (checkboxes.length === 0) { + showToast("No files selected for download."); + return; + } + window.filesToDownload = Array.from(checkboxes).map(chk => chk.value); + document.getElementById("downloadZipModal").style.display = "block"; + setTimeout(() => { + const input = document.getElementById("zipFileNameInput"); + input.focus(); + }, 100); +}; + +export function handleExtractZipSelected(e) { + if (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (!checkboxes.length) { + showToast("No files selected."); + return; + } + const zipFiles = Array.from(checkboxes) + .map(chk => chk.value) + .filter(name => name.toLowerCase().endsWith(".zip")); + if (!zipFiles.length) { + showToast("No zip files selected."); + return; + } + fetch("extractZip.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ + folder: window.currentFolder || "root", + files: zipFiles + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + let toastMessage = "Zip file(s) extracted successfully!"; + if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) { + toastMessage = "Extracted: " + data.extractedFiles.join(", "); + } + showToast(toastMessage); + loadFileList(window.currentFolder); + } else { + showToast("Error extracting zip: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error extracting zip files:", error); + showToast("Error extracting zip files."); + }); +} + +const extractZipBtn = document.getElementById("extractZipBtn"); +if (extractZipBtn) { + extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); + document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); +} + +document.addEventListener("DOMContentLoaded", function () { + const cancelDownloadZip = document.getElementById("cancelDownloadZip"); + if (cancelDownloadZip) { + cancelDownloadZip.addEventListener("click", function () { + document.getElementById("downloadZipModal").style.display = "none"; + }); + } + + const confirmDownloadZip = document.getElementById("confirmDownloadZip"); + if (confirmDownloadZip) { + confirmDownloadZip.addEventListener("click", function () { + let zipName = document.getElementById("zipFileNameInput").value.trim(); + if (!zipName) { + showToast("Please enter a name for the zip file."); + return; + } + if (!zipName.toLowerCase().endsWith(".zip")) { + zipName += ".zip"; + } + document.getElementById("downloadZipModal").style.display = "none"; + const folder = window.currentFolder || "root"; + fetch("downloadZip.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ folder: folder, files: window.filesToDownload }) + }) + .then(response => { + if (!response.ok) { + return response.text().then(text => { + throw new Error("Failed to create zip file: " + text); + }); + } + return response.blob(); + }) + .then(blob => { + if (!blob || blob.size === 0) { + throw new Error("Received empty zip file."); + } + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.style.display = "none"; + a.href = url; + a.download = zipName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + showToast("Download started."); + }) + .catch(error => { + console.error("Error downloading zip:", error); + showToast("Error downloading selected files as zip: " + error.message); + }); + }); + } +}); + +export function handleCopySelected(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (checkboxes.length === 0) { + showToast("No files selected for copying.", 5000); + return; + } + window.filesToCopy = Array.from(checkboxes).map(chk => chk.value); + document.getElementById("copyFilesModal").style.display = "block"; + loadCopyMoveFolderListForModal("copyTargetFolder"); +} + +export async function loadCopyMoveFolderListForModal(dropdownId) { + const folderSelect = document.getElementById(dropdownId); + folderSelect.innerHTML = ""; + + if (window.userFolderOnly) { + const username = localStorage.getItem("username") || "root"; + try { + const response = await fetch("getFolderList.php?restricted=1"); + let folders = await response.json(); + if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { + folders = folders.map(item => item.folder); + } + folders = folders.filter(folder => + folder.toLowerCase() !== "trash" && + (folder === username || folder.indexOf(username + "/") === 0) + ); + + const rootOption = document.createElement("option"); + rootOption.value = username; + rootOption.textContent = formatFolderName(username); + folderSelect.appendChild(rootOption); + + folders.forEach(folder => { + if (folder !== username) { + const option = document.createElement("option"); + option.value = folder; + option.textContent = formatFolderName(folder); + folderSelect.appendChild(option); + } + }); + } catch (error) { + console.error("Error loading folder list for modal:", error); + } + return; + } + + try { + const response = await fetch("getFolderList.php"); + let folders = await response.json(); + if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { + folders = folders.map(item => item.folder); + } + folders = folders.filter(folder => folder !== "root" && folder.toLowerCase() !== "trash"); + + const rootOption = document.createElement("option"); + rootOption.value = "root"; + rootOption.textContent = "(Root)"; + folderSelect.appendChild(rootOption); + + if (Array.isArray(folders) && folders.length > 0) { + folders.forEach(folder => { + const option = document.createElement("option"); + option.value = folder; + option.textContent = folder; + folderSelect.appendChild(option); + }); + } + } catch (error) { + console.error("Error loading folder list for modal:", error); + } +} + +export function handleMoveSelected(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (checkboxes.length === 0) { + showToast("No files selected for moving."); + return; + } + window.filesToMove = Array.from(checkboxes).map(chk => chk.value); + document.getElementById("moveFilesModal").style.display = "block"; + loadCopyMoveFolderListForModal("moveTargetFolder"); +} + +document.addEventListener("DOMContentLoaded", function () { + const cancelCopy = document.getElementById("cancelCopyFiles"); + if (cancelCopy) { + cancelCopy.addEventListener("click", function () { + document.getElementById("copyFilesModal").style.display = "none"; + window.filesToCopy = []; + }); + } + const confirmCopy = document.getElementById("confirmCopyFiles"); + if (confirmCopy) { + confirmCopy.addEventListener("click", function () { + const targetFolder = document.getElementById("copyTargetFolder").value; + if (!targetFolder) { + showToast("Please select a target folder for copying.", 5000); + return; + } + if (targetFolder === window.currentFolder) { + showToast("Error: Cannot copy files to the same folder."); + return; + } + fetch("copyFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ + source: window.currentFolder, + files: window.filesToCopy, + destination: targetFolder + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files copied successfully!", 5000); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not copy files"), 5000); + } + }) + .catch(error => console.error("Error copying files:", error)) + .finally(() => { + document.getElementById("copyFilesModal").style.display = "none"; + window.filesToCopy = []; + }); + }); + } +}); + +document.addEventListener("DOMContentLoaded", function () { + const cancelMove = document.getElementById("cancelMoveFiles"); + if (cancelMove) { + cancelMove.addEventListener("click", function () { + document.getElementById("moveFilesModal").style.display = "none"; + window.filesToMove = []; + }); + } + const confirmMove = document.getElementById("confirmMoveFiles"); + if (confirmMove) { + confirmMove.addEventListener("click", function () { + const targetFolder = document.getElementById("moveTargetFolder").value; + if (!targetFolder) { + showToast("Please select a target folder for moving."); + return; + } + if (targetFolder === window.currentFolder) { + showToast("Error: Cannot move files to the same folder."); + return; + } + fetch("moveFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ + source: window.currentFolder, + files: window.filesToMove, + destination: targetFolder + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Selected files moved successfully!"); + loadFileList(window.currentFolder); + } else { + showToast("Error: " + (data.error || "Could not move files")); + } + }) + .catch(error => console.error("Error moving files:", error)) + .finally(() => { + document.getElementById("moveFilesModal").style.display = "none"; + window.filesToMove = []; + }); + }); + } +}); + +export function renameFile(oldName, folder) { + window.fileToRename = oldName; + window.fileFolder = folder || window.currentFolder || "root"; + document.getElementById("newFileName").value = oldName; + document.getElementById("renameFileModal").style.display = "block"; + setTimeout(() => { + const input = document.getElementById("newFileName"); + input.focus(); + const lastDot = oldName.lastIndexOf('.'); + if (lastDot > 0) { + input.setSelectionRange(0, lastDot); + } else { + input.select(); + } + }, 100); +} + +document.addEventListener("DOMContentLoaded", () => { + const cancelBtn = document.getElementById("cancelRenameFile"); + if (cancelBtn) { + cancelBtn.addEventListener("click", function () { + document.getElementById("renameFileModal").style.display = "none"; + document.getElementById("newFileName").value = ""; + }); + } + + const submitBtn = document.getElementById("submitRenameFile"); + if (submitBtn) { + submitBtn.addEventListener("click", function () { + const newName = document.getElementById("newFileName").value.trim(); + if (!newName || newName === window.fileToRename) { + document.getElementById("renameFileModal").style.display = "none"; + return; + } + const folderUsed = window.fileFolder; + fetch("renameFile.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("File renamed successfully!"); + loadFileList(folderUsed); + } else { + showToast("Error renaming file: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error renaming file:", error); + showToast("Error renaming file"); + }) + .finally(() => { + document.getElementById("renameFileModal").style.display = "none"; + document.getElementById("newFileName").value = ""; + }); + }); + } +}); + +// Expose initFileActions so it can be called from fileManager.js +export function initFileActions() { + const deleteSelectedBtn = document.getElementById("deleteSelectedBtn"); + if (deleteSelectedBtn) { + deleteSelectedBtn.replaceWith(deleteSelectedBtn.cloneNode(true)); + document.getElementById("deleteSelectedBtn").addEventListener("click", handleDeleteSelected); + } + const copySelectedBtn = document.getElementById("copySelectedBtn"); + if (copySelectedBtn) { + copySelectedBtn.replaceWith(copySelectedBtn.cloneNode(true)); + document.getElementById("copySelectedBtn").addEventListener("click", handleCopySelected); + } + const moveSelectedBtn = document.getElementById("moveSelectedBtn"); + if (moveSelectedBtn) { + moveSelectedBtn.replaceWith(moveSelectedBtn.cloneNode(true)); + document.getElementById("moveSelectedBtn").addEventListener("click", handleMoveSelected); + } + const downloadZipBtn = document.getElementById("downloadZipBtn"); + if (downloadZipBtn) { + downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true)); + document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected); + } + const extractZipBtn = document.getElementById("extractZipBtn"); + if (extractZipBtn) { + extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); + document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); + } +} + +window.renameFile = renameFile; \ No newline at end of file diff --git a/js/fileDragDrop.js b/js/fileDragDrop.js new file mode 100644 index 0000000..426acd0 --- /dev/null +++ b/js/fileDragDrop.js @@ -0,0 +1,125 @@ +// dragDrop.js +import { showToast } from './domUtils.js'; +import { loadFileList } from './fileListView.js'; + +export function fileDragStartHandler(event) { + const row = event.currentTarget; + let fileNames = []; + + const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); + if (selectedCheckboxes.length > 1) { + selectedCheckboxes.forEach(chk => { + const parentRow = chk.closest("tr"); + if (parentRow) { + const cell = parentRow.querySelector("td:nth-child(2)"); + if (cell) { + let rawName = cell.textContent.trim(); + const tagContainer = cell.querySelector(".tag-badges"); + if (tagContainer) { + const tagText = tagContainer.innerText.trim(); + if (rawName.endsWith(tagText)) { + rawName = rawName.slice(0, -tagText.length).trim(); + } + } + fileNames.push(rawName); + } + } + }); + } else { + const fileNameCell = row.querySelector("td:nth-child(2)"); + if (fileNameCell) { + let rawName = fileNameCell.textContent.trim(); + const tagContainer = fileNameCell.querySelector(".tag-badges"); + if (tagContainer) { + const tagText = tagContainer.innerText.trim(); + if (rawName.endsWith(tagText)) { + rawName = rawName.slice(0, -tagText.length).trim(); + } + } + fileNames.push(rawName); + } + } + + if (fileNames.length === 0) return; + + const dragData = fileNames.length === 1 + ? { fileName: fileNames[0], sourceFolder: window.currentFolder || "root" } + : { files: fileNames, sourceFolder: window.currentFolder || "root" }; + + event.dataTransfer.setData("application/json", JSON.stringify(dragData)); + + let dragImage = document.createElement("div"); + dragImage.style.display = "inline-flex"; + dragImage.style.width = "auto"; + dragImage.style.maxWidth = "fit-content"; + dragImage.style.padding = "6px 10px"; + dragImage.style.backgroundColor = "#333"; + dragImage.style.color = "#fff"; + dragImage.style.border = "1px solid #555"; + dragImage.style.borderRadius = "4px"; + dragImage.style.alignItems = "center"; + dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)"; + const icon = document.createElement("span"); + icon.className = "material-icons"; + icon.textContent = "insert_drive_file"; + icon.style.marginRight = "4px"; + const label = document.createElement("span"); + label.textContent = fileNames.length === 1 ? fileNames[0] : fileNames.length + " files"; + dragImage.appendChild(icon); + dragImage.appendChild(label); + + document.body.appendChild(dragImage); + event.dataTransfer.setDragImage(dragImage, 5, 5); + setTimeout(() => { + document.body.removeChild(dragImage); + }, 0); +} + +export function folderDragOverHandler(event) { + event.preventDefault(); + event.currentTarget.classList.add("drop-hover"); +} + +export function folderDragLeaveHandler(event) { + event.currentTarget.classList.remove("drop-hover"); +} + +export function folderDropHandler(event) { + event.preventDefault(); + event.currentTarget.classList.remove("drop-hover"); + const dropFolder = event.currentTarget.getAttribute("data-folder"); + let dragData; + try { + dragData = JSON.parse(event.dataTransfer.getData("application/json")); + } catch (e) { + console.error("Invalid drag data"); + return; + } + if (!dragData || !dragData.fileName) return; + fetch("moveFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") + }, + body: JSON.stringify({ + source: dragData.sourceFolder, + files: [dragData.fileName], + destination: dropFolder + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(`File "${dragData.fileName}" moved successfully to ${dropFolder}!`); + loadFileList(dragData.sourceFolder); + } else { + showToast("Error moving file: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error moving file via drop:", error); + showToast("Error moving file."); + }); +} \ No newline at end of file diff --git a/js/fileEditor.js b/js/fileEditor.js new file mode 100644 index 0000000..c4dff5f --- /dev/null +++ b/js/fileEditor.js @@ -0,0 +1,178 @@ +// editor.js +import { showToast } from './domUtils.js'; +import { loadFileList } from './fileListView.js'; + +function getModeForFile(fileName) { + const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); + switch (ext) { + case "css": + return "css"; + case "json": + return { name: "javascript", json: true }; + case "js": + return "javascript"; + case "html": + case "htm": + return "text/html"; + case "xml": + return "xml"; + default: + return "text/plain"; + } +} +export { getModeForFile }; + +function adjustEditorSize() { + const modal = document.querySelector(".editor-modal"); + if (modal && window.currentEditor) { + const headerHeight = 60; // adjust as needed + const availableHeight = modal.clientHeight - headerHeight; + window.currentEditor.setSize("100%", availableHeight + "px"); + } +} +export { adjustEditorSize }; + +function observeModalResize(modal) { + if (!modal) return; + const resizeObserver = new ResizeObserver(() => { + adjustEditorSize(); + }); + resizeObserver.observe(modal); +} +export { observeModalResize }; + +export function editFile(fileName, folder) { + let existingEditor = document.getElementById("editorContainer"); + if (existingEditor) { + existingEditor.remove(); + } + const folderUsed = folder || window.currentFolder || "root"; + const folderPath = folderUsed === "root" + ? "uploads/" + : "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/"; + const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime(); + + fetch(fileUrl, { method: "HEAD" }) + .then(response => { + const contentLength = response.headers.get("Content-Length"); + if (contentLength !== null && parseInt(contentLength) > 10485760) { + showToast("This file is larger than 10 MB and cannot be edited in the browser."); + throw new Error("File too large."); + } + return fetch(fileUrl); + }) + .then(response => { + if (!response.ok) { + throw new Error("HTTP error! Status: " + response.status); + } + return response.text(); + }) + .then(content => { + const modal = document.createElement("div"); + modal.id = "editorContainer"; + modal.classList.add("modal", "editor-modal"); + modal.innerHTML = ` +
+

Editing: ${fileName}

+
+ + +
+ +
+ + + `; + document.body.appendChild(modal); + modal.style.display = "block"; + + const mode = getModeForFile(fileName); + const isDarkMode = document.body.classList.contains("dark-mode"); + const theme = isDarkMode ? "material-darker" : "default"; + + const editor = CodeMirror.fromTextArea(document.getElementById("fileEditor"), { + lineNumbers: true, + mode: mode, + theme: theme, + viewportMargin: Infinity + }); + + window.currentEditor = editor; + + setTimeout(() => { + adjustEditorSize(); + }, 50); + + observeModalResize(modal); + + let currentFontSize = 14; + editor.getWrapperElement().style.fontSize = currentFontSize + "px"; + editor.refresh(); + + document.getElementById("closeEditorX").addEventListener("click", function () { + modal.remove(); + }); + + document.getElementById("decreaseFont").addEventListener("click", function () { + currentFontSize = Math.max(8, currentFontSize - 2); + editor.getWrapperElement().style.fontSize = currentFontSize + "px"; + editor.refresh(); + }); + + document.getElementById("increaseFont").addEventListener("click", function () { + currentFontSize = Math.min(32, currentFontSize + 2); + editor.getWrapperElement().style.fontSize = currentFontSize + "px"; + editor.refresh(); + }); + + document.getElementById("saveBtn").addEventListener("click", function () { + saveFile(fileName, folderUsed); + }); + + document.getElementById("closeBtn").addEventListener("click", function () { + modal.remove(); + }); + + function updateEditorTheme() { + const isDarkMode = document.body.classList.contains("dark-mode"); + editor.setOption("theme", isDarkMode ? "material-darker" : "default"); + } + + document.getElementById("darkModeToggle").addEventListener("click", updateEditorTheme); + }) + .catch(error => console.error("Error loading file:", error)); +} + + +export function saveFile(fileName, folder) { + const editor = window.currentEditor; + if (!editor) { + console.error("Editor not found!"); + return; + } + const folderUsed = folder || window.currentFolder || "root"; + const fileDataObj = { + fileName: fileName, + content: editor.getValue(), + folder: folderUsed + }; + fetch("saveFile.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify(fileDataObj) + }) + .then(response => response.json()) + .then(result => { + showToast(result.success || result.error); + document.getElementById("editorContainer")?.remove(); + loadFileList(folderUsed); + }) + .catch(error => console.error("Error saving file:", error)); +} \ No newline at end of file diff --git a/js/fileListView.js b/js/fileListView.js new file mode 100644 index 0000000..89c41c9 --- /dev/null +++ b/js/fileListView.js @@ -0,0 +1,406 @@ +// fileListView.js +import { + escapeHTML, + debounce, + buildSearchAndPaginationControls, + buildFileTableHeader, + buildFileTableRow, + buildBottomControls, + updateFileActionButtons, + showToast, + updateRowHighlight, + toggleRowSelection, + attachEnterKeyListener +} from './domUtils.js'; + +import { bindFileListContextMenu } from './fileMenu.js'; + +export let fileData = []; +export let sortOrder = { column: "uploaded", ascending: true }; + +window.itemsPerPage = window.itemsPerPage || 10; +window.currentPage = window.currentPage || 1; +window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery" + +// ----------------------------- +// VIEW MODE TOGGLE BUTTON & Helpers +// ----------------------------- +export function createViewToggleButton() { + let toggleBtn = document.getElementById("toggleViewBtn"); + if (!toggleBtn) { + toggleBtn = document.createElement("button"); + toggleBtn.id = "toggleViewBtn"; + toggleBtn.classList.add("btn", "btn-secondary"); + const titleElem = document.getElementById("fileListTitle"); + if (titleElem) { + titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling); + } + } + toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; + toggleBtn.onclick = () => { + window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; + localStorage.setItem("viewMode", window.viewMode); + loadFileList(window.currentFolder); + toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; + }; + return toggleBtn; +} + +export function formatFolderName(folder) { + if (folder === "root") return "(Root)"; + return folder + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, char => char.toUpperCase()); +} + +// Expose inline DOM helpers. +window.toggleRowSelection = toggleRowSelection; +window.updateRowHighlight = updateRowHighlight; + +import { openTagModal, openMultiTagModal } from './fileTags.js'; + +// ----------------------------- +// FILE LIST & VIEW RENDERING +// ----------------------------- +export function loadFileList(folderParam) { + const folder = folderParam || "root"; + const fileListContainer = document.getElementById("fileList"); + + fileListContainer.style.visibility = "hidden"; + fileListContainer.innerHTML = "
Loading files...
"; + + return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) + .then(response => { + if (response.status === 401) { + showToast("Session expired. Please log in again."); + window.location.href = "logout.php"; + throw new Error("Unauthorized"); + } + return response.json(); + }) + .then(data => { + fileListContainer.innerHTML = ""; + if (data.files && data.files.length > 0) { + data.files = data.files.map(file => { + file.fullName = (file.path || file.name).trim().toLowerCase(); + file.editable = canEditFile(file.name); + file.folder = folder; + if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) { + file.type = "image"; + } + return file; + }); + fileData = data.files; + if (window.viewMode === "gallery") { + renderGalleryView(folder); + } else { + renderFileTable(folder); + } + } else { + fileListContainer.textContent = "No files found."; + updateFileActionButtons(); + } + return data.files || []; + }) + .catch(error => { + console.error("Error loading file list:", error); + if (error.message !== "Unauthorized") { + fileListContainer.textContent = "Error loading files."; + } + return []; + }) + .finally(() => { + fileListContainer.style.visibility = "visible"; + }); +} + +export function renderFileTable(folder) { + const fileListContainer = document.getElementById("fileList"); + const searchTerm = (window.currentSearchTerm || "").toLowerCase(); + const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10); + let currentPage = window.currentPage || 1; + + const filteredFiles = fileData.filter(file => { + const nameMatch = file.name.toLowerCase().includes(searchTerm); + const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)); + return nameMatch || tagMatch; + }); + + const totalFiles = filteredFiles.length; + const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); + if (currentPage > totalPages) { + currentPage = totalPages > 0 ? totalPages : 1; + window.currentPage = currentPage; + } + + const folderPath = folder === "root" + ? "uploads/" + : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; + + const topControlsHTML = buildSearchAndPaginationControls({ + currentPage, + totalPages, + searchTerm: window.currentSearchTerm || "" + }); + let headerHTML = buildFileTableHeader(sortOrder); + const startIndex = (currentPage - 1) * itemsPerPageSetting; + const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); + let rowsHTML = ""; + + if (totalFiles > 0) { + filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { + let rowHTML = buildFileTableRow(file, folderPath); + rowHTML = rowHTML.replace(" 0) { + tagBadgesHTML = '
'; + file.tags.forEach(tag => { + tagBadgesHTML += `${escapeHTML(tag.name)}`; + }); + tagBadgesHTML += "
"; + } + + rowHTML = rowHTML.replace(/()(.*?)(<\/td>)/, (match, p1, p2, p3) => { + return p1 + p2 + tagBadgesHTML + p3; + }); + + rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `$1`); + + rowsHTML += rowHTML; + }); + } else { + rowsHTML += `No files found.`; + } + rowsHTML += ""; + const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); + fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; + + createViewToggleButton(); + + const newSearchInput = document.getElementById("searchInput"); + if (newSearchInput) { + newSearchInput.addEventListener("input", debounce(function () { + window.currentSearchTerm = newSearchInput.value; + window.currentPage = 1; + renderFileTable(folder); + setTimeout(() => { + const freshInput = document.getElementById("searchInput"); + if (freshInput) { + freshInput.focus(); + const len = freshInput.value.length; + freshInput.setSelectionRange(len, len); + } + }, 0); + }, 300)); + } + + document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { + cell.addEventListener("click", function () { + const column = this.getAttribute("data-column"); + sortFiles(column, folder); + }); + }); + + document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { + checkbox.addEventListener("change", function (e) { + updateRowHighlight(e.target); + updateFileActionButtons(); + }); + }); + + document.querySelectorAll(".share-btn").forEach(btn => { + btn.addEventListener("click", function (e) { + e.stopPropagation(); + const fileName = this.getAttribute("data-file"); + const file = fileData.find(f => f.name === fileName); + if (file) { + import('./filePreview.js').then(module => { + module.openShareModal(file, folder); + }); + } + }); + }); + + updateFileActionButtons(); + + // Add drag-and-drop support for each table row. + document.querySelectorAll("#fileList tbody tr").forEach(row => { + row.setAttribute("draggable", "true"); + import('./fileDragDrop.js').then(module => { + row.addEventListener("dragstart", module.fileDragStartHandler); + }); + }); + + // Prevent clicks on these buttons from selecting the row + document.querySelectorAll(".download-btn, .edit-btn, .rename-btn").forEach(btn => { + btn.addEventListener("click", e => e.stopPropagation()); + }); + + // re‑bind context menu + bindFileListContextMenu(); +} + +export function renderGalleryView(folder) { + const fileListContainer = document.getElementById("fileList"); + const searchTerm = (window.currentSearchTerm || "").toLowerCase(); + const filteredFiles = fileData.filter(file => { + return file.name.toLowerCase().includes(searchTerm) || + (file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm))); + }); + + const folderPath = folder === "root" + ? "uploads/" + : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; + const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;"; + let galleryHTML = `"; + fileListContainer.innerHTML = galleryHTML; + + createViewToggleButton(); + updateFileActionButtons(); +} + +export function sortFiles(column, folder) { + if (sortOrder.column === column) { + sortOrder.ascending = !sortOrder.ascending; + } else { + sortOrder.column = column; + sortOrder.ascending = true; + } + fileData.sort((a, b) => { + let valA = a[column] || ""; + let valB = b[column] || ""; + if (column === "modified" || column === "uploaded") { + const parsedA = parseCustomDate(valA); + const parsedB = parseCustomDate(valB); + valA = parsedA; + valB = parsedB; + } else if (typeof valA === "string") { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + if (valA < valB) return sortOrder.ascending ? -1 : 1; + if (valA > valB) return sortOrder.ascending ? 1 : -1; + return 0; + }); + if (window.viewMode === "gallery") { + renderGalleryView(folder); + } else { + renderFileTable(folder); + } +} + +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(); +} + +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); +} + +// Expose global functions for pagination and preview. +window.changePage = function (newPage) { + window.currentPage = newPage; + renderFileTable(window.currentFolder); +}; +window.changeItemsPerPage = function (newCount) { + window.itemsPerPage = parseInt(newCount); + window.currentPage = 1; + renderFileTable(window.currentFolder); +}; + +// fileListView.js (bottom) + +window.loadFileList = loadFileList; +window.renderFileTable = renderFileTable; +window.renderGalleryView = renderGalleryView; +window.sortFiles = sortFiles; \ No newline at end of file diff --git a/js/fileManager.js b/js/fileManager.js index a177796..3e06e39 100644 --- a/js/fileManager.js +++ b/js/fileManager.js @@ -1,1652 +1,40 @@ -import { - escapeHTML, - debounce, - buildSearchAndPaginationControls, - buildFileTableHeader, - buildFileTableRow, - buildBottomControls, - updateFileActionButtons, - showToast, - updateRowHighlight, - toggleRowSelection, - attachEnterKeyListener -} from './domUtils.js'; - -export let fileData = []; -export let sortOrder = { column: "uploaded", ascending: true }; - -import { initTagSearch, openTagModal, openMultiTagModal } from './fileTags.js'; - -window.itemsPerPage = window.itemsPerPage || 10; -window.currentPage = window.currentPage || 1; -window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery" - -// ============================== -// VIEW MODE TOGGLE BUTTON -// ============================== -function createViewToggleButton() { - let toggleBtn = document.getElementById("toggleViewBtn"); - if (!toggleBtn) { - toggleBtn = document.createElement("button"); - toggleBtn.id = "toggleViewBtn"; - toggleBtn.classList.add("btn", "btn-secondary"); - const titleElem = document.getElementById("fileListTitle"); - if (titleElem) { - titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling); - } - } - toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; - toggleBtn.onclick = () => { - window.viewMode = window.viewMode === "gallery" ? "table" : "gallery"; - localStorage.setItem("viewMode", window.viewMode); - loadFileList(window.currentFolder); - toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View"; - }; - return toggleBtn; -} -window.createViewToggleButton = createViewToggleButton; - -// ----------------------------- -// Helper: formatFolderName -// ----------------------------- -function formatFolderName(folder) { - if (folder === "root") return "(Root)"; - return folder - .replace(/[_-]+/g, " ") - .replace(/\b\w/g, char => char.toUpperCase()); -} - -// Expose inline DOM helpers. -window.toggleRowSelection = toggleRowSelection; -window.updateRowHighlight = updateRowHighlight; - -// ============================================== -// FEATURE: Public File Sharing Modal -// ============================================== -function openShareModal(file, folder) { - const existing = document.getElementById("shareModal"); - if (existing) existing.remove(); - - const modal = document.createElement("div"); - modal.id = "shareModal"; - modal.classList.add("modal"); - modal.innerHTML = ` - - `; - document.body.appendChild(modal); - modal.style.display = "block"; - - document.getElementById("closeShareModal").addEventListener("click", () => { - modal.remove(); - }); - - document.getElementById("generateShareLinkBtn").addEventListener("click", () => { - const expiration = document.getElementById("shareExpiration").value; - const password = document.getElementById("sharePassword").value; - fetch("createShareLink.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ - folder: folder, - file: file.name, - expirationMinutes: parseInt(expiration), - password: password - }) - }) - .then(response => response.json()) - .then(data => { - if (data.token) { - let shareEndpoint = document.querySelector('meta[name="share-url"]') - ? document.querySelector('meta[name="share-url"]').getAttribute('content') - : (window.SHARE_URL || "share.php"); - const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`; - const displayDiv = document.getElementById("shareLinkDisplay"); - const inputField = document.getElementById("shareLinkInput"); - inputField.value = shareUrl; - displayDiv.style.display = "block"; - } else { - showToast("Error generating share link: " + (data.error || "Unknown error")); - } - }) - .catch(err => { - console.error("Error generating share link:", err); - showToast("Error generating share link."); - }); - }); - - document.getElementById("copyShareLinkBtn").addEventListener("click", () => { - const input = document.getElementById("shareLinkInput"); - input.select(); - document.execCommand("copy"); - showToast("Link copied to clipboard!"); - }); -} - -// ============================================== -// FEATURE: Enhanced Preview Modal with Navigation -// ============================================== -function previewFile(fileUrl, fileName) { - let modal = document.getElementById("filePreviewModal"); - if (!modal) { - modal = document.createElement("div"); - modal.id = "filePreviewModal"; - Object.assign(modal.style, { - 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); - - function closeModal() { - // Pause media elements without resetting currentTime for video elements - const mediaElements = modal.querySelectorAll("video, audio"); - mediaElements.forEach(media => { - media.pause(); - // Only reset if it's not a video - if (media.tagName.toLowerCase() !== 'video') { - try { - media.currentTime = 0; - } catch(e) { - // Some media types might not support setting currentTime. - } - } - }); - modal.style.display = "none"; - } - - document.getElementById("closeFileModal").addEventListener("click", closeModal); - modal.addEventListener("click", function (e) { - if (e.target === modal) { - closeModal(); - } - }); - } - modal.querySelector("h4").textContent = fileName; - const container = modal.querySelector(".file-preview-container"); - container.innerHTML = ""; - - const extension = fileName.split('.').pop().toLowerCase(); - const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName); - if (isImage) { - const img = document.createElement("img"); - img.src = fileUrl; - img.className = "image-modal-img"; - img.style.maxWidth = "80vw"; - img.style.maxHeight = "80vh"; - container.appendChild(img); - - const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)); - if (images.length > 1) { - modal.galleryImages = images; - modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName); - - const prevBtn = document.createElement("button"); - prevBtn.textContent = "‹"; - prevBtn.className = "gallery-nav-btn"; - prevBtn.style.cssText = "position: absolute; top: 50%; left: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;"; - prevBtn.addEventListener("click", function (e) { - e.stopPropagation(); - modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length; - let newFile = modal.galleryImages[modal.galleryCurrentIndex]; - modal.querySelector("h4").textContent = newFile.name; - img.src = ((window.currentFolder === "root") - ? "uploads/" - : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") - + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); - }); - const nextBtn = document.createElement("button"); - nextBtn.textContent = "›"; - nextBtn.className = "gallery-nav-btn"; - nextBtn.style.cssText = "position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;"; - nextBtn.addEventListener("click", function (e) { - e.stopPropagation(); - modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length; - let newFile = modal.galleryImages[modal.galleryCurrentIndex]; - modal.querySelector("h4").textContent = newFile.name; - img.src = ((window.currentFolder === "root") - ? "uploads/" - : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") - + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); - }); - container.appendChild(prevBtn); - container.appendChild(nextBtn); - } - } else { - if (extension === "pdf") { - const embed = document.createElement("embed"); - 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|mkv|webm|mov|ogv)$/i.test(fileName)) { - const video = document.createElement("video"); - video.src = fileUrl; - video.controls = true; - video.className = "image-modal-img"; - - // Create a unique key for this video (using fileUrl here) - const progressKey = 'videoProgress-' + fileUrl; - - // When the video's metadata is loaded, check for saved progress - video.addEventListener("loadedmetadata", () => { - const savedTime = localStorage.getItem(progressKey); - if (savedTime) { - video.currentTime = parseFloat(savedTime); - } - }); - - // Listen for time updates and save the current time - video.addEventListener("timeupdate", () => { - localStorage.setItem(progressKey, video.currentTime); - }); - - video.addEventListener("ended", () => { - localStorage.removeItem(progressKey); - }); - - container.appendChild(video); - - } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) { - const audio = document.createElement("audio"); - audio.src = fileUrl; - audio.controls = true; - audio.className = "audio-modal"; - audio.style.maxWidth = "80vw"; - container.appendChild(audio); - } else { - container.textContent = "Preview not available for this file type."; - } - } - modal.style.display = "flex"; -} - -// ============================================== -// ORIGINAL FILE MANAGER FUNCTIONS -// ============================================== -export function loadFileList(folderParam) { - const folder = folderParam || "root"; - const fileListContainer = document.getElementById("fileList"); - - fileListContainer.style.visibility = "hidden"; - fileListContainer.innerHTML = "
Loading files...
"; - - return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) - .then(response => { - if (response.status === 401) { - showToast("Session expired. Please log in again."); - window.location.href = "logout.php"; - throw new Error("Unauthorized"); - } - return response.json(); - }) - .then(data => { - fileListContainer.innerHTML = ""; - if (data.files && data.files.length > 0) { - data.files = data.files.map(file => { - file.fullName = (file.path || file.name).trim().toLowerCase(); - file.editable = canEditFile(file.name); - file.folder = folder; - if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) { - file.type = "image"; - } - return file; - }); - fileData = data.files; - if (window.viewMode === "gallery") { - renderGalleryView(folder); - } else { - renderFileTable(folder); - } - } else { - fileListContainer.textContent = "No files found."; - updateFileActionButtons(); - } - return data.files || []; - }) - .catch(error => { - console.error("Error loading file list:", error); - if (error.message !== "Unauthorized") { - fileListContainer.textContent = "Error loading files."; - } - return []; - }) - .finally(() => { - fileListContainer.style.visibility = "visible"; - }); -} - -// -// --- DRAG & DROP SUPPORT FOR FILE ROWS --- -// -function fileDragStartHandler(event) { - const row = event.currentTarget; - let fileNames = []; - - // Check if multiple file checkboxes are selected. - const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); - if (selectedCheckboxes.length > 1) { - selectedCheckboxes.forEach(chk => { - const parentRow = chk.closest("tr"); - if (parentRow) { - const cell = parentRow.querySelector("td:nth-child(2)"); - if (cell) { - let rawName = cell.textContent.trim(); - // Attempt to get the tag text from a container that holds the tags. - const tagContainer = cell.querySelector(".tag-badges"); - if (tagContainer) { - const tagText = tagContainer.innerText.trim(); - if (rawName.endsWith(tagText)) { - rawName = rawName.slice(0, -tagText.length).trim(); - } - } - fileNames.push(rawName); - } - } - }); - } else { - const fileNameCell = row.querySelector("td:nth-child(2)"); - if (fileNameCell) { - let rawName = fileNameCell.textContent.trim(); - const tagContainer = fileNameCell.querySelector(".tag-badges"); - if (tagContainer) { - const tagText = tagContainer.innerText.trim(); - if (rawName.endsWith(tagText)) { - rawName = rawName.slice(0, -tagText.length).trim(); - } - } - fileNames.push(rawName); - } - } - - if (fileNames.length === 0) return; - - // For a single file, send fileName; for multiple, send an array. - const dragData = fileNames.length === 1 - ? { fileName: fileNames[0], sourceFolder: window.currentFolder || "root" } - : { files: fileNames, sourceFolder: window.currentFolder || "root" }; - - event.dataTransfer.setData("application/json", JSON.stringify(dragData)); - - // Create a custom drag image. - let dragImage = document.createElement("div"); - dragImage.style.display = "inline-flex"; - dragImage.style.width = "auto"; - dragImage.style.maxWidth = "fit-content"; - dragImage.style.padding = "6px 10px"; - dragImage.style.backgroundColor = "#333"; - dragImage.style.color = "#fff"; - dragImage.style.border = "1px solid #555"; - dragImage.style.borderRadius = "4px"; - dragImage.style.alignItems = "center"; - dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)"; - const icon = document.createElement("span"); - icon.className = "material-icons"; - icon.textContent = "insert_drive_file"; - icon.style.marginRight = "4px"; - const label = document.createElement("span"); - label.textContent = fileNames.length === 1 ? fileNames[0] : fileNames.length + " files"; - dragImage.appendChild(icon); - dragImage.appendChild(label); - - document.body.appendChild(dragImage); - event.dataTransfer.setDragImage(dragImage, 5, 5); - setTimeout(() => { - document.body.removeChild(dragImage); - }, 0); -} - -// -// --- RENDER FILE TABLE (TABLE VIEW) --- -// -export function renderFileTable(folder) { - const fileListContainer = document.getElementById("fileList"); - const searchTerm = (window.currentSearchTerm || "").toLowerCase(); - const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10); - let currentPage = window.currentPage || 1; - - // Filter files: include a file if its name OR any of its tags include the search term. - const filteredFiles = fileData.filter(file => { - const nameMatch = file.name.toLowerCase().includes(searchTerm); - const tagMatch = file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm)); - return nameMatch || tagMatch; - }); - - const totalFiles = filteredFiles.length; - const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); - if (currentPage > totalPages) { - currentPage = totalPages > 0 ? totalPages : 1; - window.currentPage = currentPage; - } - - const folderPath = folder === "root" - ? "uploads/" - : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; - - const topControlsHTML = buildSearchAndPaginationControls({ - currentPage, - totalPages, - searchTerm: window.currentSearchTerm || "" - }); - let headerHTML = buildFileTableHeader(sortOrder); - const startIndex = (currentPage - 1) * itemsPerPageSetting; - const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); - let rowsHTML = ""; - - if (totalFiles > 0) { - filteredFiles.slice(startIndex, endIndex).forEach((file, idx) => { - // Build the table row HTML. - let rowHTML = buildFileTableRow(file, folderPath); - // Add a unique id attribute so that tag updates can target this row. - rowHTML = rowHTML.replace(" 0) { - tagBadgesHTML = '
'; - file.tags.forEach(tag => { - tagBadgesHTML += `${escapeHTML(tag.name)}`; - }); - tagBadgesHTML += "
"; - } - - // Insert tag badges into the file name cell. - rowHTML = rowHTML.replace(/()(.*?)(<\/td>)/, (match, p1, p2, p3) => { - return p1 + p2 + tagBadgesHTML + p3; - }); - - // Insert share button into the actions cell. - rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `$1`); - - rowsHTML += rowHTML; - }); - } else { - rowsHTML += `No files found.`; - } - rowsHTML += ""; - const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); - fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; - - createViewToggleButton(); - - const newSearchInput = document.getElementById("searchInput"); - if (newSearchInput) { - newSearchInput.addEventListener("input", debounce(function () { - window.currentSearchTerm = newSearchInput.value; - window.currentPage = 1; - renderFileTable(folder); - setTimeout(() => { - const freshInput = document.getElementById("searchInput"); - if (freshInput) { - freshInput.focus(); - const len = freshInput.value.length; - freshInput.setSelectionRange(len, len); - } - }, 0); - }, 300)); - } - - document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { - cell.addEventListener("click", function () { - const column = this.getAttribute("data-column"); - sortFiles(column, folder); - }); - }); - - document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { - checkbox.addEventListener("change", function (e) { - updateRowHighlight(e.target); - updateFileActionButtons(); - }); - }); - - document.querySelectorAll(".share-btn").forEach(btn => { - btn.addEventListener("click", function (e) { - e.stopPropagation(); - const fileName = this.getAttribute("data-file"); - const file = fileData.find(f => f.name === fileName); - if (file) { - openShareModal(file, folder); - } - }); - }); - - updateFileActionButtons(); - - // Add drag-and-drop support for each table row. - document.querySelectorAll("#fileList tbody tr").forEach(row => { - row.setAttribute("draggable", "true"); - row.addEventListener("dragstart", fileDragStartHandler); - }); -} - -export function renderGalleryView(folder) { - const fileListContainer = document.getElementById("fileList"); - const searchTerm = (window.currentSearchTerm || "").toLowerCase(); - // Filter files using the same logic as table view. - const filteredFiles = fileData.filter(file => { - return file.name.toLowerCase().includes(searchTerm) || - (file.tags && file.tags.some(tag => tag.name.toLowerCase().includes(searchTerm))); - }); - - const folderPath = folder === "root" - ? "uploads/" - : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; - const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;"; - let galleryHTML = `"; - fileListContainer.innerHTML = galleryHTML; - - createViewToggleButton(); - updateFileActionButtons(); -} - -// -// --- SORT FILES & PARSE DATE --- -// -export function sortFiles(column, folder) { - if (sortOrder.column === column) { - sortOrder.ascending = !sortOrder.ascending; - } else { - sortOrder.column = column; - sortOrder.ascending = true; - } - fileData.sort((a, b) => { - let valA = a[column] || ""; - let valB = b[column] || ""; - if (column === "modified" || column === "uploaded") { - const parsedA = parseCustomDate(valA); - const parsedB = parseCustomDate(valB); - valA = parsedA; - valB = parsedB; - } else if (typeof valA === "string") { - valA = valA.toLowerCase(); - valB = valB.toLowerCase(); - } - if (valA < valB) return sortOrder.ascending ? -1 : 1; - if (valA > valB) return sortOrder.ascending ? 1 : -1; - return 0; - }); - if (window.viewMode === "gallery") { - renderGalleryView(folder); - } else { - renderFileTable(folder); - } -} - -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(); -} - -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); -} - -// -// --- FILE ACTIONS: DELETE, DOWNLOAD, COPY, MOVE --- -// -export function handleDeleteSelected(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (checkboxes.length === 0) { - showToast("No files selected."); - return; - } - window.filesToDelete = Array.from(checkboxes).map(chk => chk.value); - document.getElementById("deleteFilesMessage").textContent = - "Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?"; - document.getElementById("deleteFilesModal").style.display = "block"; - attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles"); -} +// fileManager.js +import './fileListView.js'; +import './filePreview.js'; +import './fileEditor.js'; +import './fileDragDrop.js'; +import './fileMenu.js'; +import { initFileActions } from './fileActions.js'; +// Initialize file action buttons. document.addEventListener("DOMContentLoaded", function () { - const cancelDelete = document.getElementById("cancelDeleteFiles"); - if (cancelDelete) { - cancelDelete.addEventListener("click", function () { - document.getElementById("deleteFilesModal").style.display = "none"; - window.filesToDelete = []; - }); - } - - const confirmDelete = document.getElementById("confirmDeleteFiles"); - if (confirmDelete) { - confirmDelete.addEventListener("click", function () { - fetch("deleteFiles.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("Selected files deleted successfully!"); - loadFileList(window.currentFolder); - } else { - showToast("Error: " + (data.error || "Could not delete files")); - } - }) - .catch(error => console.error("Error deleting files:", error)) - .finally(() => { - document.getElementById("deleteFilesModal").style.display = "none"; - window.filesToDelete = []; - }); - }); - } -}); -attachEnterKeyListener("downloadZipModal", "confirmDownloadZip"); -export function handleDownloadZipSelected(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (checkboxes.length === 0) { - showToast("No files selected for download."); - return; - } - window.filesToDownload = Array.from(checkboxes).map(chk => chk.value); - document.getElementById("downloadZipModal").style.display = "block"; - setTimeout(() => { - const input = document.getElementById("zipFileNameInput"); - input.focus(); - }, 100); - -} - -export function handleExtractZipSelected(e) { - if (e) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - // Get selected file names - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (!checkboxes.length) { - showToast("No files selected."); - return; - } - // Filter for zip files only - const zipFiles = Array.from(checkboxes) - .map(chk => chk.value) - .filter(name => name.toLowerCase().endsWith(".zip")); - if (!zipFiles.length) { - showToast("No zip files selected."); - return; - } - // Call the extract endpoint with the selected zip files - fetch("extractZip.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ - folder: window.currentFolder || "root", - files: zipFiles - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // If the server returned a list of extracted files, join them into a string. - let toastMessage = "Zip file(s) extracted successfully!"; - if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) { - toastMessage = "Extracted: " + data.extractedFiles.join(", "); - } - showToast(toastMessage); - loadFileList(window.currentFolder); - } else { - showToast("Error extracting zip: " + (data.error || "Unknown error")); - } - }) - .catch(error => { - console.error("Error extracting zip files:", error); - showToast("Error extracting zip files."); - }); -} - -const extractZipBtn = document.getElementById("extractZipBtn"); -if (extractZipBtn) { - extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); - document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); -} - -document.addEventListener("DOMContentLoaded", function () { - const cancelDownloadZip = document.getElementById("cancelDownloadZip"); - if (cancelDownloadZip) { - cancelDownloadZip.addEventListener("click", function () { - document.getElementById("downloadZipModal").style.display = "none"; - }); - } - - const confirmDownloadZip = document.getElementById("confirmDownloadZip"); - if (confirmDownloadZip) { - confirmDownloadZip.addEventListener("click", function () { - let zipName = document.getElementById("zipFileNameInput").value.trim(); - if (!zipName) { - showToast("Please enter a name for the zip file."); - return; - } - if (!zipName.toLowerCase().endsWith(".zip")) { - zipName += ".zip"; - } - document.getElementById("downloadZipModal").style.display = "none"; - const folder = window.currentFolder || "root"; - fetch("downloadZip.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ folder: folder, files: window.filesToDownload }) - }) - .then(response => { - if (!response.ok) { - return response.text().then(text => { - throw new Error("Failed to create zip file: " + text); - }); - } - return response.blob(); - }) - .then(blob => { - if (!blob || blob.size === 0) { - throw new Error("Received empty zip file."); - } - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.style.display = "none"; - a.href = url; - a.download = zipName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - a.remove(); - showToast("Download started."); - }) - .catch(error => { - console.error("Error downloading zip:", error); - showToast("Error downloading selected files as zip: " + error.message); - }); - }); - } + initFileActions(); }); -export function handleCopySelected(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (checkboxes.length === 0) { - showToast("No files selected for copying.", 5000); - return; - } - window.filesToCopy = Array.from(checkboxes).map(chk => chk.value); - document.getElementById("copyFilesModal").style.display = "block"; - loadCopyMoveFolderListForModal("copyTargetFolder"); -} - -export async function loadCopyMoveFolderListForModal(dropdownId) { - const folderSelect = document.getElementById(dropdownId); - folderSelect.innerHTML = ""; - - // Check if the user is restricted to their personal folder. - if (window.userFolderOnly) { - const username = localStorage.getItem("username") || "root"; - try { - // Fetch only the user's folders (assuming getFolderList.php?restricted=1 returns only folders for the user). - const response = await fetch("getFolderList.php?restricted=1"); - let folders = await response.json(); - // If folders come as objects, extract the folder names. - if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { - folders = folders.map(item => item.folder); - } - // Filter out folders named "trash" (case insensitive) and include only folders that are the user's folder or start with username + "/" - folders = folders.filter(folder => - folder.toLowerCase() !== "trash" && - (folder === username || folder.indexOf(username + "/") === 0) - ); - - // Always include the user's root folder as the first option. - const rootOption = document.createElement("option"); - rootOption.value = username; - rootOption.textContent = formatFolderName(username); - folderSelect.appendChild(rootOption); - - // Add any subfolders (if they exist). - folders.forEach(folder => { - if (folder !== username) { // Avoid duplicating the root. - const option = document.createElement("option"); - option.value = folder; - option.textContent = formatFolderName(folder); - folderSelect.appendChild(option); - } - }); - } catch (error) { - console.error("Error loading folder list for modal:", error); - } - return; - } - - // If not folder-only, fetch and show the full list (excluding "root" and "trash"). - try { - const response = await fetch("getFolderList.php"); - let folders = await response.json(); - if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { - folders = folders.map(item => item.folder); - } - folders = folders.filter(folder => folder !== "root" && folder.toLowerCase() !== "trash"); - - // Add a "(Root)" option. - const rootOption = document.createElement("option"); - rootOption.value = "root"; - rootOption.textContent = "(Root)"; - folderSelect.appendChild(rootOption); - - if (Array.isArray(folders) && folders.length > 0) { - folders.forEach(folder => { - const option = document.createElement("option"); - option.value = folder; - option.textContent = formatFolderName(folder); - folderSelect.appendChild(option); - }); - } - } catch (error) { - console.error("Error loading folder list for modal:", error); - } -} - -export function handleMoveSelected(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (checkboxes.length === 0) { - showToast("No files selected for moving."); - return; - } - window.filesToMove = Array.from(checkboxes).map(chk => chk.value); - document.getElementById("moveFilesModal").style.display = "block"; - loadCopyMoveFolderListForModal("moveTargetFolder"); -} - -document.addEventListener("DOMContentLoaded", function () { - const cancelCopy = document.getElementById("cancelCopyFiles"); - if (cancelCopy) { - cancelCopy.addEventListener("click", function () { - document.getElementById("copyFilesModal").style.display = "none"; - window.filesToCopy = []; - }); - } - const confirmCopy = document.getElementById("confirmCopyFiles"); - if (confirmCopy) { - confirmCopy.addEventListener("click", function () { - const targetFolder = document.getElementById("copyTargetFolder").value; - if (!targetFolder) { - showToast("Please select a target folder for copying.", 5000); - return; - } - if (targetFolder === window.currentFolder) { - showToast("Error: Cannot copy files to the same folder."); - return; - } - fetch("copyFiles.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ - source: window.currentFolder, - files: window.filesToCopy, - destination: targetFolder - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("Selected files copied successfully!", 5000); - loadFileList(window.currentFolder); - } else { - showToast("Error: " + (data.error || "Could not copy files"), 5000); - } - }) - .catch(error => console.error("Error copying files:", error)) - .finally(() => { - document.getElementById("copyFilesModal").style.display = "none"; - window.filesToCopy = []; - }); - }); - } -}); - -document.addEventListener("DOMContentLoaded", function () { - const cancelMove = document.getElementById("cancelMoveFiles"); - if (cancelMove) { - cancelMove.addEventListener("click", function () { - document.getElementById("moveFilesModal").style.display = "none"; - window.filesToMove = []; - }); - } - const confirmMove = document.getElementById("confirmMoveFiles"); - if (confirmMove) { - confirmMove.addEventListener("click", function () { - const targetFolder = document.getElementById("moveTargetFolder").value; - if (!targetFolder) { - showToast("Please select a target folder for moving."); - return; - } - if (targetFolder === window.currentFolder) { - showToast("Error: Cannot move files to the same folder."); - return; - } - fetch("moveFiles.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ - source: window.currentFolder, - files: window.filesToMove, - destination: targetFolder - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("Selected files moved successfully!"); - loadFileList(window.currentFolder); - } else { - showToast("Error: " + (data.error || "Could not move files")); - } - }) - .catch(error => console.error("Error moving files:", error)) - .finally(() => { - document.getElementById("moveFilesModal").style.display = "none"; - window.filesToMove = []; - }); - }); - } -}); - -// -// --- FOLDER TREE DRAG & DROP SUPPORT --- -// When a draggable file is dragged over a folder node, allow the drop and highlight it. -function folderDragOverHandler(event) { - event.preventDefault(); - event.currentTarget.classList.add("drop-hover"); -} - -function folderDragLeaveHandler(event) { - event.currentTarget.classList.remove("drop-hover"); -} - -function folderDropHandler(event) { - event.preventDefault(); - event.currentTarget.classList.remove("drop-hover"); - const dropFolder = event.currentTarget.getAttribute("data-folder"); - let dragData; - try { - dragData = JSON.parse(event.dataTransfer.getData("application/json")); - } catch (e) { - console.error("Invalid drag data"); - return; - } - if (!dragData || !dragData.fileName) return; - fetch("moveFiles.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") - }, - body: JSON.stringify({ - source: dragData.sourceFolder, - files: [dragData.fileName], - destination: dropFolder - }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast(`File "${dragData.fileName}" moved successfully to ${dropFolder}!`); - loadFileList(dragData.sourceFolder); - } else { - showToast("Error moving file: " + (data.error || "Unknown error")); - } - }) - .catch(error => { - console.error("Error moving file via drop:", error); - showToast("Error moving file."); - }); -} - -// -// --- CODEMIRROR EDITOR & UTILITY FUNCTIONS --- -// -function getModeForFile(fileName) { - const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); - switch (ext) { - case "css": - return "css"; - case "json": - return { name: "javascript", json: true }; - case "js": - return "javascript"; - case "html": - case "htm": - return "text/html"; - case "xml": - return "xml"; - default: - return "text/plain"; - } -} - -function adjustEditorSize() { - const modal = document.querySelector(".editor-modal"); - if (modal && window.currentEditor) { - // Calculate available height for the editor. - // If you have a header or footer inside the modal, subtract their heights. - const headerHeight = 60; // adjust this value as needed - const availableHeight = modal.clientHeight - headerHeight; - window.currentEditor.setSize("100%", availableHeight + "px"); - } -} - -function observeModalResize(modal) { - if (!modal) return; - const resizeObserver = new ResizeObserver(() => { - adjustEditorSize(); - }); - resizeObserver.observe(modal); -} - -export function editFile(fileName, folder) { - let existingEditor = document.getElementById("editorContainer"); - if (existingEditor) { - existingEditor.remove(); - } - const folderUsed = folder || window.currentFolder || "root"; - const folderPath = folderUsed === "root" - ? "uploads/" - : "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/"; - const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime(); - - fetch(fileUrl, { method: "HEAD" }) - .then(response => { - const contentLength = response.headers.get("Content-Length"); - if (contentLength !== null && parseInt(contentLength) > 10485760) { - showToast("This file is larger than 10 MB and cannot be edited in the browser."); - throw new Error("File too large."); - } - return fetch(fileUrl); - }) - .then(response => { - if (!response.ok) { - throw new Error("HTTP error! Status: " + response.status); - } - return response.text(); - }) - .then(content => { - const modal = document.createElement("div"); - modal.id = "editorContainer"; - modal.classList.add("modal", "editor-modal"); - modal.innerHTML = ` -
-

Editing: ${fileName}

-
- - -
- -
- - - `; - document.body.appendChild(modal); - modal.style.display = "block"; - - const mode = getModeForFile(fileName); - const isDarkMode = document.body.classList.contains("dark-mode"); - const theme = isDarkMode ? "material-darker" : "default"; - - const editor = CodeMirror.fromTextArea(document.getElementById("fileEditor"), { - lineNumbers: true, - mode: mode, - theme: theme, - viewportMargin: Infinity - }); - - window.currentEditor = editor; - - setTimeout(() => { - adjustEditorSize(); - }, 50); - - observeModalResize(modal); - - let currentFontSize = 14; - editor.getWrapperElement().style.fontSize = currentFontSize + "px"; - editor.refresh(); - - document.getElementById("closeEditorX").addEventListener("click", function () { - modal.remove(); - }); - - document.getElementById("decreaseFont").addEventListener("click", function () { - currentFontSize = Math.max(8, currentFontSize - 2); - editor.getWrapperElement().style.fontSize = currentFontSize + "px"; - editor.refresh(); - }); - - document.getElementById("increaseFont").addEventListener("click", function () { - currentFontSize = Math.min(32, currentFontSize + 2); - editor.getWrapperElement().style.fontSize = currentFontSize + "px"; - editor.refresh(); - }); - - document.getElementById("saveBtn").addEventListener("click", function () { - saveFile(fileName, folderUsed); - }); - - document.getElementById("closeBtn").addEventListener("click", function () { - modal.remove(); - }); - - function updateEditorTheme() { - const isDarkMode = document.body.classList.contains("dark-mode"); - editor.setOption("theme", isDarkMode ? "material-darker" : "default"); - } - - document.getElementById("darkModeToggle").addEventListener("click", updateEditorTheme); - }) - .catch(error => console.error("Error loading file:", error)); -} - -export function saveFile(fileName, folder) { - const editor = window.currentEditor; - if (!editor) { - console.error("Editor not found!"); - return; - } - const folderUsed = folder || window.currentFolder || "root"; - const fileDataObj = { - fileName: fileName, - content: editor.getValue(), - folder: folderUsed - }; - fetch("saveFile.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify(fileDataObj) - }) - .then(response => response.json()) - .then(result => { - showToast(result.success || result.error); - document.getElementById("editorContainer")?.remove(); - loadFileList(folderUsed); - }) - .catch(error => console.error("Error saving file:", error)); -} - -export function displayFilePreview(file, container) { - // Use the underlying File object if it exists (for resumable files) - const actualFile = file.file || file; - container.style.display = "inline-block"; - if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) { - const img = document.createElement("img"); - img.src = URL.createObjectURL(actualFile); - img.classList.add("file-preview-img"); - container.innerHTML = ""; // Clear previous content - container.appendChild(img); - } else { - container.innerHTML = ""; // Clear previous content - const iconSpan = document.createElement("span"); - iconSpan.classList.add("material-icons", "file-icon"); - iconSpan.textContent = "insert_drive_file"; - container.appendChild(iconSpan); - } -} - -export function initFileActions() { - const deleteSelectedBtn = document.getElementById("deleteSelectedBtn"); - if (deleteSelectedBtn) { - deleteSelectedBtn.replaceWith(deleteSelectedBtn.cloneNode(true)); - document.getElementById("deleteSelectedBtn").addEventListener("click", handleDeleteSelected); - } - const copySelectedBtn = document.getElementById("copySelectedBtn"); - if (copySelectedBtn) { - copySelectedBtn.replaceWith(copySelectedBtn.cloneNode(true)); - document.getElementById("copySelectedBtn").addEventListener("click", handleCopySelected); - } - const moveSelectedBtn = document.getElementById("moveSelectedBtn"); - if (moveSelectedBtn) { - moveSelectedBtn.replaceWith(moveSelectedBtn.cloneNode(true)); - document.getElementById("moveSelectedBtn").addEventListener("click", handleMoveSelected); - } - const downloadZipBtn = document.getElementById("downloadZipBtn"); - if (downloadZipBtn) { - downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true)); - document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected); - } - const extractZipBtn = document.getElementById("extractZipBtn"); - if (extractZipBtn) { - extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); - document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); - } -} -attachEnterKeyListener("renameFileModal", "submitRenameFile"); -export function renameFile(oldName, folder) { - window.fileToRename = oldName; - window.fileFolder = folder || window.currentFolder || "root"; - document.getElementById("newFileName").value = oldName; - document.getElementById("renameFileModal").style.display = "block"; - setTimeout(() => { - const input = document.getElementById("newFileName"); - input.focus(); - const lastDot = oldName.lastIndexOf('.'); - if (lastDot > 0) { - input.setSelectionRange(0, lastDot); - } else { - input.select(); - } - }, 100); -} - -document.addEventListener("DOMContentLoaded", () => { - const cancelBtn = document.getElementById("cancelRenameFile"); - if (cancelBtn) { - cancelBtn.addEventListener("click", function () { - document.getElementById("renameFileModal").style.display = "none"; - document.getElementById("newFileName").value = ""; - }); - } - - const submitBtn = document.getElementById("submitRenameFile"); - if (submitBtn) { - submitBtn.addEventListener("click", function () { - const newName = document.getElementById("newFileName").value.trim(); - if (!newName || newName === window.fileToRename) { - document.getElementById("renameFileModal").style.display = "none"; - return; - } - const folderUsed = window.fileFolder; - fetch("renameFile.php", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "X-CSRF-Token": window.csrfToken - }, - body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showToast("File renamed successfully!"); - loadFileList(folderUsed); - } else { - showToast("Error renaming file: " + (data.error || "Unknown error")); - } - }) - .catch(error => { - console.error("Error renaming file:", error); - showToast("Error renaming file"); - }) - .finally(() => { - document.getElementById("renameFileModal").style.display = "none"; - document.getElementById("newFileName").value = ""; - }); - }); - } -}); - -window.renameFile = renameFile; -window.changePage = function (newPage) { - window.currentPage = newPage; - renderFileTable(window.currentFolder); -}; -window.changeItemsPerPage = function (newCount) { - window.itemsPerPage = parseInt(newCount); - window.currentPage = 1; - renderFileTable(window.currentFolder); -}; -window.previewFile = previewFile; - -// -// --- Expose Drag-Drop Support for Folder Tree Nodes --- -// (Attach dragover, dragleave, and drop events to folder tree nodes) +// Attach folder drag-and-drop support for folder tree nodes. document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll(".folder-option").forEach(el => { - el.addEventListener("dragover", folderDragOverHandler); - el.addEventListener("dragleave", folderDragLeaveHandler); - el.addEventListener("drop", folderDropHandler); + import('./fileDragDrop.js').then(module => { + el.addEventListener("dragover", module.folderDragOverHandler); + el.addEventListener("dragleave", module.folderDragLeaveHandler); + el.addEventListener("drop", module.folderDropHandler); + }); }); }); +// Global keydown listener for file deletion via Delete/Backspace. document.addEventListener("keydown", function(e) { - // Skip if focus is on an input, textarea, or any contentEditable element. const tag = e.target.tagName.toLowerCase(); if (tag === "input" || tag === "textarea" || e.target.isContentEditable) { return; } - // On Mac, the delete key is often reported as "Backspace" (keyCode 8) if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) { const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); if (selectedCheckboxes.length > 0) { - e.preventDefault(); // Prevent default back navigation in some browsers. - handleDeleteSelected(new Event("click")); - } - } -}); - -// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ---------- - -// Function to display the context menu with provided items at (x, y) -// Function to display the context menu with provided items at (x, y) -function showFileContextMenu(x, y, menuItems) { - let menu = document.getElementById("fileContextMenu"); - if (!menu) { - menu = document.createElement("div"); - menu.id = "fileContextMenu"; - // Use fixed positioning so the menu is relative to the viewport - menu.style.position = "fixed"; - menu.style.backgroundColor = "#fff"; - menu.style.border = "1px solid #ccc"; - menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)"; - menu.style.zIndex = "9999"; - menu.style.padding = "5px 0"; - menu.style.minWidth = "150px"; - document.body.appendChild(menu); - } - // Clear previous items - menu.innerHTML = ""; - menuItems.forEach(item => { - let menuItem = document.createElement("div"); - menuItem.textContent = item.label; - menuItem.style.padding = "5px 15px"; - menuItem.style.cursor = "pointer"; - menuItem.addEventListener("mouseover", () => { - menuItem.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#444" : "#f0f0f0"; - }); - menuItem.addEventListener("mouseout", () => { - menuItem.style.backgroundColor = ""; - }); - menuItem.addEventListener("click", () => { - item.action(); - hideFileContextMenu(); - }); - menu.appendChild(menuItem); - }); - - // Use the event's clientX and clientY coordinates (which are viewport-relative) - menu.style.left = x + "px"; - menu.style.top = y + "px"; - menu.style.display = "block"; - - // Adjust if the menu would extend past the bottom of the viewport - const menuRect = menu.getBoundingClientRect(); - const viewportHeight = window.innerHeight; - if (menuRect.bottom > viewportHeight) { - let newTop = viewportHeight - menuRect.height; - if (newTop < 0) newTop = 0; - menu.style.top = newTop + "px"; - } -} - -function hideFileContextMenu() { - const menu = document.getElementById("fileContextMenu"); - if (menu) { - menu.style.display = "none"; - } -} - -// Context menu handler for the file list. -function fileListContextMenuHandler(e) { - e.preventDefault(); - - // If no file is selected, try to select the row that was right-clicked. - let row = e.target.closest("tr"); - if (row) { - const checkbox = row.querySelector(".file-checkbox"); - if (checkbox && !checkbox.checked) { - checkbox.checked = true; - updateRowHighlight(checkbox); - updateFileActionButtons(); - } - } - - // Get selected file names. - const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value); - - // Build the context menu items. - let menuItems = [ - { label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } }, - { label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } }, - { label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } }, - { label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } } - ]; - - if (selected.some(name => name.toLowerCase().endsWith(".zip"))) { - menuItems.push({ - label: "Extract Zip", - action: () => { handleExtractZipSelected(new Event("click")); } - }); - } - - // If multiple files are selected, add a "Tag Selected" option. - if (selected.length > 1) { - menuItems.push({ - label: "Tag Selected", - action: () => { - const files = fileData.filter(f => selected.includes(f.name)); - openMultiTagModal(files); - } - }); - } - // If exactly one file is selected, add options specific to that file. - else if (selected.length === 1) { - const file = fileData.find(f => f.name === selected[0]); - - menuItems.push({ - label: "Preview", - action: () => { - const folder = window.currentFolder || "root"; - const folderPath = folder === "root" - ? "uploads/" - : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; - previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name); - } - }); - - if (canEditFile(file.name)) { - menuItems.push({ - label: "Edit", - action: () => { editFile(selected[0], window.currentFolder); } + e.preventDefault(); + import('./fileActions.js').then(module => { + module.handleDeleteSelected(new Event("click")); }); } - - menuItems.push({ - label: "Rename", - action: () => { renameFile(selected[0], window.currentFolder); } - }); - - menuItems.push({ - label: "Tag File", - action: () => { openTagModal(file); } - }); } - - showFileContextMenu(e.clientX, e.clientY, menuItems); -} - -// Bind the context menu to the file list container. -// (This is set every time the file list is rendered.) -function bindFileListContextMenu() { - const fileListContainer = document.getElementById("fileList"); - if (fileListContainer) { - fileListContainer.oncontextmenu = fileListContextMenuHandler; - } -} - -// Hide the context menu if clicking anywhere else. -document.addEventListener("click", function(e) { - const menu = document.getElementById("fileContextMenu"); - if (menu && menu.style.display === "block") { - hideFileContextMenu(); - } -}); - -// After rendering the file table, bind the context menu handler. -(function() { - const originalRenderFileTable = renderFileTable; - renderFileTable = function(folder) { - originalRenderFileTable(folder); - bindFileListContextMenu(); - }; -})(); \ No newline at end of file +}); \ No newline at end of file diff --git a/js/fileMenu.js b/js/fileMenu.js new file mode 100644 index 0000000..5adc863 --- /dev/null +++ b/js/fileMenu.js @@ -0,0 +1,155 @@ +// contextMenu.js +import { updateRowHighlight, showToast } from './domUtils.js'; +import { handleDeleteSelected, handleCopySelected, handleMoveSelected, handleDownloadZipSelected, handleExtractZipSelected, renameFile } from './fileActions.js'; +import { previewFile } from './filePreview.js'; +import { editFile } from './fileEditor.js'; +import { canEditFile, fileData } from './fileListView.js'; +import { openTagModal, openMultiTagModal } from './fileTags.js'; + +export function showFileContextMenu(x, y, menuItems) { + let menu = document.getElementById("fileContextMenu"); + if (!menu) { + menu = document.createElement("div"); + menu.id = "fileContextMenu"; + menu.style.position = "fixed"; + menu.style.backgroundColor = "#fff"; + menu.style.border = "1px solid #ccc"; + menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)"; + menu.style.zIndex = "9999"; + menu.style.padding = "5px 0"; + menu.style.minWidth = "150px"; + document.body.appendChild(menu); + } + menu.innerHTML = ""; + menuItems.forEach(item => { + let menuItem = document.createElement("div"); + menuItem.textContent = item.label; + menuItem.style.padding = "5px 15px"; + menuItem.style.cursor = "pointer"; + menuItem.addEventListener("mouseover", () => { + menuItem.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#444" : "#f0f0f0"; + }); + menuItem.addEventListener("mouseout", () => { + menuItem.style.backgroundColor = ""; + }); + menuItem.addEventListener("click", () => { + item.action(); + hideFileContextMenu(); + }); + menu.appendChild(menuItem); + }); + + menu.style.left = x + "px"; + menu.style.top = y + "px"; + menu.style.display = "block"; + + const menuRect = menu.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + if (menuRect.bottom > viewportHeight) { + let newTop = viewportHeight - menuRect.height; + if (newTop < 0) newTop = 0; + menu.style.top = newTop + "px"; + } +} + +export function hideFileContextMenu() { + const menu = document.getElementById("fileContextMenu"); + if (menu) { + menu.style.display = "none"; + } +} + +export function fileListContextMenuHandler(e) { + e.preventDefault(); + + let row = e.target.closest("tr"); + if (row) { + const checkbox = row.querySelector(".file-checkbox"); + if (checkbox && !checkbox.checked) { + checkbox.checked = true; + updateRowHighlight(checkbox); + } + } + + const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value); + + let menuItems = [ + { label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } }, + { label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } }, + { label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } }, + { label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } } + ]; + + if (selected.some(name => name.toLowerCase().endsWith(".zip"))) { + menuItems.push({ + label: "Extract Zip", + action: () => { handleExtractZipSelected(new Event("click")); } + }); + } + + if (selected.length > 1) { + menuItems.push({ + label: "Tag Selected", + action: () => { + const files = fileData.filter(f => selected.includes(f.name)); + openMultiTagModal(files); + } + }); + } + else if (selected.length === 1) { + const file = fileData.find(f => f.name === selected[0]); + + menuItems.push({ + label: "Preview", + action: () => { + const folder = window.currentFolder || "root"; + const folderPath = folder === "root" + ? "uploads/" + : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; + previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name); + } + }); + + if (canEditFile(file.name)) { + menuItems.push({ + label: "Edit", + action: () => { editFile(selected[0], window.currentFolder); } + }); + } + + menuItems.push({ + label: "Rename", + action: () => { renameFile(selected[0], window.currentFolder); } + }); + + menuItems.push({ + label: "Tag File", + action: () => { openTagModal(file); } + }); + } + + showFileContextMenu(e.clientX, e.clientY, menuItems); +} + +export function bindFileListContextMenu() { + const fileListContainer = document.getElementById("fileList"); + if (fileListContainer) { + fileListContainer.oncontextmenu = fileListContextMenuHandler; + } +} + +document.addEventListener("click", function(e) { + const menu = document.getElementById("fileContextMenu"); + if (menu && menu.style.display === "block") { + hideFileContextMenu(); + } +}); + +// Rebind context menu after file table render. +(function() { + const originalRenderFileTable = window.renderFileTable; + window.renderFileTable = function(folder) { + originalRenderFileTable(folder); + bindFileListContextMenu(); + }; +})(); \ No newline at end of file diff --git a/js/filePreview.js b/js/filePreview.js new file mode 100644 index 0000000..07325cb --- /dev/null +++ b/js/filePreview.js @@ -0,0 +1,256 @@ +// filePreview.js +import { escapeHTML, showToast } from './domUtils.js'; +import { fileData } from './fileListView.js'; + +export function openShareModal(file, folder) { + const existing = document.getElementById("shareModal"); + if (existing) existing.remove(); + + const modal = document.createElement("div"); + modal.id = "shareModal"; + modal.classList.add("modal"); + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + modal.style.display = "block"; + + document.getElementById("closeShareModal").addEventListener("click", () => { + modal.remove(); + }); + + document.getElementById("generateShareLinkBtn").addEventListener("click", () => { + const expiration = document.getElementById("shareExpiration").value; + const password = document.getElementById("sharePassword").value; + fetch("createShareLink.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ + folder: folder, + file: file.name, + expirationMinutes: parseInt(expiration), + password: password + }) + }) + .then(response => response.json()) + .then(data => { + if (data.token) { + let shareEndpoint = document.querySelector('meta[name="share-url"]') + ? document.querySelector('meta[name="share-url"]').getAttribute('content') + : (window.SHARE_URL || "share.php"); + const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`; + const displayDiv = document.getElementById("shareLinkDisplay"); + const inputField = document.getElementById("shareLinkInput"); + inputField.value = shareUrl; + displayDiv.style.display = "block"; + } else { + showToast("Error generating share link: " + (data.error || "Unknown error")); + } + }) + .catch(err => { + console.error("Error generating share link:", err); + showToast("Error generating share link."); + }); + }); + + document.getElementById("copyShareLinkBtn").addEventListener("click", () => { + const input = document.getElementById("shareLinkInput"); + input.select(); + document.execCommand("copy"); + showToast("Link copied to clipboard!"); + }); +} + +export function previewFile(fileUrl, fileName) { + let modal = document.getElementById("filePreviewModal"); + if (!modal) { + modal = document.createElement("div"); + modal.id = "filePreviewModal"; + Object.assign(modal.style, { + 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); + + function closeModal() { + const mediaElements = modal.querySelectorAll("video, audio"); + mediaElements.forEach(media => { + media.pause(); + if (media.tagName.toLowerCase() !== 'video') { + try { + media.currentTime = 0; + } catch(e) { } + } + }); + modal.style.display = "none"; + } + + document.getElementById("closeFileModal").addEventListener("click", closeModal); + modal.addEventListener("click", function (e) { + if (e.target === modal) { + closeModal(); + } + }); + } + modal.querySelector("h4").textContent = fileName; + const container = modal.querySelector(".file-preview-container"); + container.innerHTML = ""; + + const extension = fileName.split('.').pop().toLowerCase(); + const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName); + if (isImage) { + const img = document.createElement("img"); + img.src = fileUrl; + img.className = "image-modal-img"; + img.style.maxWidth = "80vw"; + img.style.maxHeight = "80vh"; + container.appendChild(img); + + const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)); + if (images.length > 1) { + modal.galleryImages = images; + modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName); + + const prevBtn = document.createElement("button"); + prevBtn.textContent = "‹"; + prevBtn.className = "gallery-nav-btn"; + prevBtn.style.cssText = "position: absolute; top: 50%; left: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;"; + prevBtn.addEventListener("click", function (e) { + e.stopPropagation(); + modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length; + let newFile = modal.galleryImages[modal.galleryCurrentIndex]; + modal.querySelector("h4").textContent = newFile.name; + img.src = ((window.currentFolder === "root") + ? "uploads/" + : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") + + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); + }); + const nextBtn = document.createElement("button"); + nextBtn.textContent = "›"; + nextBtn.className = "gallery-nav-btn"; + nextBtn.style.cssText = "position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;"; + nextBtn.addEventListener("click", function (e) { + e.stopPropagation(); + modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length; + let newFile = modal.galleryImages[modal.galleryCurrentIndex]; + modal.querySelector("h4").textContent = newFile.name; + img.src = ((window.currentFolder === "root") + ? "uploads/" + : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") + + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); + }); + container.appendChild(prevBtn); + container.appendChild(nextBtn); + } + } else { + if (extension === "pdf") { + const embed = document.createElement("embed"); + 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|mkv|webm|mov|ogv)$/i.test(fileName)) { + const video = document.createElement("video"); + video.src = fileUrl; + video.controls = true; + video.className = "image-modal-img"; + + const progressKey = 'videoProgress-' + fileUrl; + + video.addEventListener("loadedmetadata", () => { + const savedTime = localStorage.getItem(progressKey); + if (savedTime) { + video.currentTime = parseFloat(savedTime); + } + }); + + video.addEventListener("timeupdate", () => { + localStorage.setItem(progressKey, video.currentTime); + }); + + video.addEventListener("ended", () => { + localStorage.removeItem(progressKey); + }); + + container.appendChild(video); + + } else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) { + const audio = document.createElement("audio"); + audio.src = fileUrl; + audio.controls = true; + audio.className = "audio-modal"; + audio.style.maxWidth = "80vw"; + container.appendChild(audio); + } else { + container.textContent = "Preview not available for this file type."; + } + } + modal.style.display = "flex"; +} + +// Added to preserve the original functionality. +export function displayFilePreview(file, container) { + const actualFile = file.file || file; + container.style.display = "inline-block"; + if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) { + const img = document.createElement("img"); + img.src = URL.createObjectURL(actualFile); + img.classList.add("file-preview-img"); + container.innerHTML = ""; + container.appendChild(img); + } else { + container.innerHTML = ""; + const iconSpan = document.createElement("span"); + iconSpan.classList.add("material-icons", "file-icon"); + iconSpan.textContent = "insert_drive_file"; + container.appendChild(iconSpan); + } +} + +window.previewFile = previewFile; \ No newline at end of file diff --git a/js/folderManager.js b/js/folderManager.js index 1fadeed..b44dea9 100644 --- a/js/folderManager.js +++ b/js/folderManager.js @@ -1,6 +1,6 @@ // folderManager.js -import { loadFileList } from './fileManager.js'; +import { loadFileList } from './fileListView.js'; import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js'; /* ---------------------- diff --git a/js/main.js b/js/main.js index 0fb6cf9..cb4d9d6 100644 --- a/js/main.js +++ b/js/main.js @@ -1,24 +1,15 @@ import { sendRequest } from './networkUtils.js'; -import { - toggleVisibility, - toggleAllCheckboxes, - updateFileActionButtons, - showToast -} from './domUtils.js'; -import { - loadFileList, - initFileActions, - editFile, - saveFile, - displayFilePreview, - renameFile -} from './fileManager.js'; +import { toggleVisibility, toggleAllCheckboxes, updateFileActionButtons, showToast } from './domUtils.js'; import { loadFolderTree } from './folderManager.js'; import { initUpload } from './upload.js'; import { initAuth, checkAuthentication } from './auth.js'; import { setupTrashRestoreDelete } from './trashRestoreDelete.js'; import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js'; import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js'; +import { displayFilePreview } from './filePreview.js'; +import { loadFileList } from './fileListView.js'; +import { initFileActions, renameFile } from './fileActions.js'; +import { editFile, saveFile } from './fileEditor.js'; function loadCsrfTokenWithRetry(retries = 3, delay = 1000) { return fetch('token.php', { credentials: 'include' }) diff --git a/js/trashRestoreDelete.js b/js/trashRestoreDelete.js index 31f21d2..58d1f76 100644 --- a/js/trashRestoreDelete.js +++ b/js/trashRestoreDelete.js @@ -1,7 +1,7 @@ // trashRestoreDelete.js import { sendRequest } from './networkUtils.js'; import { toggleVisibility, showToast } from './domUtils.js'; -import { loadFileList } from './fileManager.js'; +import { loadFileList } from './fileListView.js'; import { loadFolderTree } from './folderManager.js'; function showConfirm(message, onConfirm) { diff --git a/js/upload.js b/js/upload.js index 6c6faf5..d7055b8 100644 --- a/js/upload.js +++ b/js/upload.js @@ -1,6 +1,8 @@ -import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js'; +import { initFileActions } from './fileActions.js'; +import { displayFilePreview } from './filePreview.js'; import { showToast, escapeHTML } from './domUtils.js'; import { loadFolderTree } from './folderManager.js'; +import { loadFileList } from './fileListView.js'; /* ----------------------------------------------------- Helpers for Drag–and–Drop Folder Uploads (Original Code)