Authentication & Initialization Changes plus File & Fold Manager Enhancements

This commit is contained in:
Ryan
2025-03-25 03:29:32 -04:00
committed by GitHub
parent c0dc0ce391
commit 49138835ce
7 changed files with 673 additions and 147 deletions

228
auth.js
View File

@@ -3,78 +3,110 @@ import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmM
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
import { loadFolderTree } from './folderManager.js';
function initAuth() {
// First, check if the user is already authenticated.
checkAuthentication(false).then(data => {
if (data.setup) {
window.setupMode = true;
showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
document.getElementById('newUsername').focus();
return;
}
window.setupMode = false;
if (data.authenticated) {
// User is logged in—show the main UI.
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
attachEnterKeyListener("addUserModal", "saveUserBtn");
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
document.querySelector(".header-buttons").style.visibility = "visible";
// If admin, show admin-only buttons.
if (data.isAdmin) {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "block";
if (removeUserBtn) removeUserBtn.style.display = "block";
// Create and show the restore button.
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
// Use a material icon.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else {
headerButtons.appendChild(restoreBtn);
}
}
/**
* Updates the select element to reflect the stored items-per-page value.
*/
function updateItemsPerPageSelect() {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
}
/**
* Updates the UI for an authenticated user.
* This includes showing the main UI panels, attaching key listeners, updating header buttons,
* and displaying admin-only buttons if applicable.
*/
function updateAuthenticatedUI(data) {
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", true);
toggleVisibility("uploadFileForm", true);
toggleVisibility("fileListContainer", true);
attachEnterKeyListener("addUserModal", "saveUserBtn");
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
document.querySelector(".header-buttons").style.visibility = "visible";
// If admin, show admin-only buttons; otherwise hide them.
if (data.isAdmin) {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "block";
if (removeUserBtn) removeUserBtn.style.display = "block";
let restoreBtn = document.getElementById("restoreFilesBtn");
if (!restoreBtn) {
restoreBtn = document.createElement("button");
restoreBtn.id = "restoreFilesBtn";
restoreBtn.classList.add("btn", "btn-warning");
// Using a material icon for restore.
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
const headerButtons = document.querySelector(".header-buttons");
if (headerButtons) {
if (headerButtons.children.length >= 5) {
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
} else {
headerButtons.appendChild(restoreBtn);
}
restoreBtn.style.display = "block";
}
}
restoreBtn.style.display = "block";
} else {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) restoreBtn.style.display = "none";
}
updateItemsPerPageSelect();
}
/**
* Checks the user's authentication state and updates the UI accordingly.
* If in setup mode or not authenticated, it shows the proper UI elements.
* When authenticated, it calls updateAuthenticatedUI to handle the UI updates.
*/
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
document.getElementById('newUsername').focus();
return false;
}
window.setupMode = false;
if (data.authenticated) {
updateAuthenticatedUI(data);
return data;
} else {
const addUserBtn = document.getElementById("addUserBtn");
const removeUserBtn = document.getElementById("removeUserBtn");
if (addUserBtn) addUserBtn.style.display = "none";
if (removeUserBtn) removeUserBtn.style.display = "none";
const restoreBtn = document.getElementById("restoreFilesBtn");
if (restoreBtn) {
restoreBtn.style.display = "none";
}
if (showLoginToast) showToast("Please log in to continue.");
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
return false;
}
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
} else {
toggleVisibility("loginForm", true);
attachEnterKeyListener("loginModal", "loginBtn");
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
}
}).catch(error => {
})
.catch(error => {
console.error("Error checking authentication:", error);
return false;
});
}
/**
* Initializes authentication by checking the user's state and setting up event listeners.
* The UI will update automatically based on the auth state.
*/
function initAuth() {
checkAuthentication(false).catch(error => {
console.error("Error checking authentication:", error);
});
@@ -83,7 +115,6 @@ function initAuth() {
if (authForm) {
authForm.addEventListener("submit", function (event) {
event.preventDefault();
// Get the "Remember me" checkbox value.
const rememberMe = document.getElementById("rememberMeCheckbox")
? document.getElementById("rememberMeCheckbox").checked
: false;
@@ -137,10 +168,8 @@ function initAuth() {
});
document.getElementById("saveUserBtn").addEventListener("click", function () {
const newUsername = document.getElementById("newUsername").value.trim();
// Use the new ID for the add user modal's password field.
const newPassword = document.getElementById("addUserPassword").value.trim();
const isAdmin = document.getElementById("isAdmin").checked;
console.log("newUsername:", newUsername, "newPassword:", newPassword);
if (!newUsername || !newPassword) {
showToast("Username and password are required!");
return;
@@ -163,6 +192,7 @@ function initAuth() {
if (data.success) {
showToast("User added successfully!");
closeAddUserModal();
// Re-check auth state to update the UI after adding a user.
checkAuthentication(false);
} else {
showToast("Error: " + (data.error || "Could not add user"));
@@ -187,14 +217,10 @@ function initAuth() {
showToast("Please select a user to remove.");
return;
}
// Await the confirmation result from your custom modal helper.
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
if (!confirmed) {
return;
}
// Proceed with deletion...
fetch("removeUser.php", {
method: "POST",
credentials: "include",
@@ -222,35 +248,27 @@ function initAuth() {
});
document.getElementById("changePasswordBtn").addEventListener("click", function () {
// Show the Change Password modal.
document.getElementById("changePasswordModal").style.display = "block";
document.getElementById("oldPassword").focus();
});
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
// Hide the Change Password modal.
document.getElementById("changePasswordModal").style.display = "none";
});
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
const oldPassword = document.getElementById("oldPassword").value.trim();
const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field
const newPassword = document.getElementById("newPassword").value.trim();
const confirmPassword = document.getElementById("confirmPassword").value.trim();
if (!oldPassword || !newPassword || !confirmPassword) {
showToast("Please fill in all fields.");
return;
}
if (newPassword !== confirmPassword) {
showToast("New passwords do not match.");
return;
}
// Prepare the data to send.
const data = { oldPassword, newPassword, confirmPassword };
// Send request to changePassword.php.
fetch("changePassword.php", {
method: "POST",
credentials: "include",
@@ -264,7 +282,6 @@ function initAuth() {
.then(result => {
if (result.success) {
showToast(result.success);
// Clear form fields and close modal.
document.getElementById("oldPassword").value = "";
document.getElementById("newPassword").value = "";
document.getElementById("confirmPassword").value = "";
@@ -280,39 +297,6 @@ function initAuth() {
});
}
function checkAuthentication(showLoginToast = true) {
return sendRequest("checkAuth.php")
.then(data => {
if (data.setup) {
window.setupMode = true;
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
toggleVisibility("loginForm", false);
toggleVisibility("mainOperations", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
toggleVisibility("addUserModal", true);
document.getElementById('newUsername').focus();
return false;
}
window.setupMode = false;
if (data.authenticated) {
return data;
} else {
if (showLoginToast) showToast("Please log in to continue.");
toggleVisibility("loginForm", true);
toggleVisibility("mainOperations", false);
toggleVisibility("uploadFileForm", false);
toggleVisibility("fileListContainer", false);
document.querySelector(".header-buttons").style.visibility = "hidden";
return false;
}
})
.catch(error => {
console.error("Error checking authentication:", error);
return false;
});
}
window.checkAuthentication = checkAuthentication;
window.changeItemsPerPage = function (value) {
localStorage.setItem("itemsPerPage", value);
const folder = window.currentFolder || "root";
@@ -322,11 +306,7 @@ window.changeItemsPerPage = function (value) {
};
document.addEventListener("DOMContentLoaded", function () {
const selectElem = document.querySelector(".form-control.bottom-select");
if (selectElem) {
const stored = localStorage.getItem("itemsPerPage") || "10";
selectElem.value = stored;
}
updateItemsPerPageSelect();
});
function resetUserForm() {

View File

@@ -28,35 +28,39 @@ export function toggleAllCheckboxes(masterCheckbox) {
}
export function updateFileActionButtons() {
const fileListContainer = document.getElementById("fileList");
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const zipBtn = document.getElementById("downloadZipBtn");
const extractZipBtn = document.getElementById("extractZipBtn");
if (fileCheckboxes.length === 0) {
if (copyBtn) copyBtn.style.display = "none";
if (moveBtn) moveBtn.style.display = "none";
if (deleteBtn) deleteBtn.style.display = "none";
if (zipBtn) zipBtn.style.display = "none";
if (extractZipBtn) extractZipBtn.style.display = "none";
} else {
if (copyBtn) copyBtn.style.display = "inline-block";
if (moveBtn) moveBtn.style.display = "inline-block";
if (deleteBtn) deleteBtn.style.display = "inline-block";
if (zipBtn) zipBtn.style.display = "inline-block";
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
if (selectedCheckboxes.length > 0) {
if (copyBtn) copyBtn.disabled = false;
if (moveBtn) moveBtn.disabled = false;
if (deleteBtn) deleteBtn.disabled = false;
if (zipBtn) zipBtn.disabled = false;
} else {
if (copyBtn) copyBtn.disabled = true;
if (moveBtn) moveBtn.disabled = true;
if (deleteBtn) deleteBtn.disabled = true;
if (zipBtn) zipBtn.disabled = true;
const anySelected = selectedCheckboxes.length > 0;
if (copyBtn) copyBtn.disabled = !anySelected;
if (moveBtn) moveBtn.disabled = !anySelected;
if (deleteBtn) deleteBtn.disabled = !anySelected;
if (zipBtn) zipBtn.disabled = !anySelected;
if (extractZipBtn) {
// Enable only if at least one selected file ends with .zip (case-insensitive).
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
chk.value.toLowerCase().endsWith(".zip")
);
extractZipBtn.disabled = !anyZipSelected;
}
}
}
@@ -310,7 +314,10 @@ export function previewFile(fileUrl, fileName) {
export function attachEnterKeyListener(modalId, buttonId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.addEventListener("keypress", function(e) {
// Make the modal focusable
modal.setAttribute("tabindex", "-1");
modal.focus();
modal.addEventListener("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
const btn = document.getElementById(buttonId);

146
extractZip.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
// --- CSRF Protection ---
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
if ($receivedToken !== $_SESSION['csrf_token']) {
http_response_code(403);
echo json_encode(["error" => "Invalid CSRF token"]);
exit;
}
// Ensure user is authenticated.
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
http_response_code(401);
echo json_encode(["error" => "Unauthorized"]);
exit;
}
// Read and decode the JSON input.
$rawData = file_get_contents("php://input");
$data = json_decode($rawData, true);
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
http_response_code(400);
echo json_encode(["error" => "Invalid input."]);
exit;
}
$folder = $data['folder'];
$files = $data['files'];
if (empty($files)) {
http_response_code(400);
echo json_encode(["error" => "No files specified."]);
exit;
}
// Validate folder name (allow "root" or valid subfolder names).
if ($folder !== "root") {
$parts = explode('/', $folder);
foreach ($parts as $part) {
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
http_response_code(400);
echo json_encode(["error" => "Invalid folder name."]);
exit;
}
}
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
} else {
$relativePath = "";
}
$baseDir = realpath(UPLOAD_DIR);
if ($baseDir === false) {
http_response_code(500);
echo json_encode(["error" => "Uploads directory not configured correctly."]);
exit;
}
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
$folderPathReal = realpath($folderPath);
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
http_response_code(404);
echo json_encode(["error" => "Folder not found."]);
exit;
}
// ---------- Metadata Setup ----------
function getMetadataFilePath($folder) {
if (strtolower($folder) === 'root' || $folder === '') {
return META_DIR . "root_metadata.json";
}
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
}
$srcMetaFile = getMetadataFilePath($folder);
$destMetaFile = getMetadataFilePath($folder);
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
$errors = [];
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
$allSuccess = true;
// ---------- Process Each File ----------
foreach ($files as $zipFileName) {
$originalName = basename(trim($zipFileName));
// Process only .zip files.
if (strtolower(substr($originalName, -4)) !== '.zip') {
continue;
}
if (!preg_match($safeFileNamePattern, $originalName)) {
$errors[] = "$originalName has an invalid name.";
$allSuccess = false;
continue;
}
$zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName;
if (!file_exists($zipFilePath)) {
$errors[] = "$originalName does not exist in folder.";
$allSuccess = false;
continue;
}
$zip = new ZipArchive();
if ($zip->open($zipFilePath) !== TRUE) {
$errors[] = "Could not open $originalName as a zip file.";
$allSuccess = false;
continue;
}
// Attempt extraction.
if (!$zip->extractTo($folderPathReal)) {
$errors[] = "Failed to extract $originalName.";
$allSuccess = false;
} else {
// Update metadata for each extracted file if the zip file has metadata.
if (isset($srcMetadata[$originalName])) {
$zipMeta = $srcMetadata[$originalName];
// Iterate through all entries in the zip.
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$extractedFileName = basename($entryName);
if ($extractedFileName) {
$destMetadata[$extractedFileName] = $zipMeta;
}
}
}
}
$zip->close();
}
// Write updated metadata back to the destination metadata file.
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
$errors[] = "Failed to update metadata.";
$allSuccess = false;
}
if ($allSuccess) {
echo json_encode(["success" => true]);
} else {
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
}
exit;
?>

View File

@@ -9,6 +9,7 @@ import {
showToast,
updateRowHighlight,
toggleRowSelection,
attachEnterKeyListener,
previewFile as originalPreviewFile
} from './domUtils.js';
@@ -661,6 +662,7 @@ export function handleDeleteSelected(e) {
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 () {
@@ -671,6 +673,7 @@ document.addEventListener("DOMContentLoaded", function () {
window.filesToDelete = [];
});
}
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
@@ -700,7 +703,7 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
});
attachEnterKeyListener("downloadZipModal", "confirmDownloadZip");
export function handleDownloadZipSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -711,6 +714,64 @@ export function handleDownloadZipSelected(e) {
}
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) {
showToast("Zip file(s) extracted successfully!");
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 () {
@@ -720,6 +781,7 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("downloadZipModal").style.display = "none";
});
}
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
if (confirmDownloadZip) {
confirmDownloadZip.addEventListener("click", function () {
@@ -1035,7 +1097,7 @@ export function editFile(fileName, folder) {
fetch(fileUrl, { method: "HEAD" })
.then(response => {
const contentLength = response.headers.get("Content-Length");
if (!contentLength || parseInt(contentLength) > 10485760) {
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.");
}
@@ -1196,8 +1258,13 @@ export function initFileActions() {
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";
@@ -1223,6 +1290,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("newFileName").value = "";
});
}
const submitBtn = document.getElementById("submitRenameFile");
if (submitBtn) {
submitBtn.addEventListener("click", function () {
@@ -1283,4 +1351,164 @@ document.addEventListener("DOMContentLoaded", function () {
el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler);
});
});
});
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 showFileContextMenu(x, y, menuItems) {
let menu = document.getElementById("fileContextMenu");
if (!menu) {
menu = document.createElement("div");
menu.id = "fileContextMenu";
menu.style.position = "absolute";
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", () => {
if (document.body.classList.contains("dark-mode")) {
menuItem.style.backgroundColor = "#444"; // darker gray for dark mode
} else {
menuItem.style.backgroundColor = "#f0f0f0"; // light gray for light mode
}
});
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";
}
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 (selected.length === 1) {
// Look up the file object.
const file = fileData.find(f => f.name === selected[0]);
// Add Preview option.
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);
}
});
// Only show Edit option if file is editable.
if (canEditFile(file.name)) {
menuItems.push({
label: "Edit",
action: () => { editFile(selected[0], window.currentFolder); }
});
}
// Add Rename option.
menuItems.push({
label: "Rename",
action: () => { renameFile(selected[0], window.currentFolder); }
});
}
showFileContextMenu(e.pageX, e.pageY, 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();
};
})();

View File

@@ -1,7 +1,7 @@
// folderManager.js
import { loadFileList } from './fileManager.js';
import { showToast, escapeHTML } from './domUtils.js';
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
// ----------------------
// Helper Functions (Data/State)
@@ -90,7 +90,6 @@ function bindBreadcrumbEvents() {
link.addEventListener("click", function (e) {
e.stopPropagation();
let folder = this.getAttribute("data-folder");
console.log("Breadcrumb clicked, folder:", folder);
window.currentFolder = folder;
localStorage.setItem("lastOpenedFolder", folder);
const titleEl = document.getElementById("fileListTitle");
@@ -447,6 +446,7 @@ export function loadFolderList(selectedFolder) {
// ----------------------
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() {
@@ -470,7 +470,7 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function
document.getElementById("renameFolderModal").style.display = "none";
document.getElementById("newRenameFolderName").value = "";
});
attachEnterKeyListener("renameFolderModal", "submitRenameFolder");
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
event.preventDefault();
const selectedFolder = window.currentFolder || "root";
@@ -527,7 +527,7 @@ function openDeleteFolderModal() {
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
document.getElementById("deleteFolderModal").style.display = "none";
});
attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
const selectedFolder = window.currentFolder || "root";
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
@@ -565,7 +565,7 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function
document.getElementById("createFolderModal").style.display = "none";
document.getElementById("newFolderName").value = "";
});
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
document.getElementById("submitCreateFolder").addEventListener("click", function () {
const folderInput = document.getElementById("newFolderName").value.trim();
if (!folderInput) {
@@ -606,4 +606,150 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
console.error("Error creating folder:", error);
document.getElementById("createFolderModal").style.display = "none";
});
});
});
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
// Function to display the custom context menu at (x, y) with given menu items.
function showFolderManagerContextMenu(x, y, menuItems) {
let menu = document.getElementById("folderManagerContextMenu");
if (!menu) {
menu = document.createElement("div");
menu.id = "folderManagerContextMenu";
menu.style.position = "absolute";
menu.style.padding = "5px 0";
menu.style.minWidth = "150px";
menu.style.zIndex = "9999";
document.body.appendChild(menu);
}
// Set styles based on dark mode.
if (document.body.classList.contains("dark-mode")) {
menu.style.backgroundColor = "#2c2c2c";
menu.style.border = "1px solid #555";
menu.style.color = "#e0e0e0";
} else {
menu.style.backgroundColor = "#fff";
menu.style.border = "1px solid #ccc";
menu.style.color = "#000";
}
// Clear previous items.
menu.innerHTML = "";
menuItems.forEach(item => {
const menuItem = document.createElement("div");
menuItem.textContent = item.label;
menuItem.style.padding = "5px 15px";
menuItem.style.cursor = "pointer";
menuItem.addEventListener("mouseover", () => {
if (document.body.classList.contains("dark-mode")) {
menuItem.style.backgroundColor = "#444";
} else {
menuItem.style.backgroundColor = "#f0f0f0";
}
});
menuItem.addEventListener("mouseout", () => {
menuItem.style.backgroundColor = "";
});
menuItem.addEventListener("click", () => {
item.action();
hideFolderManagerContextMenu();
});
menu.appendChild(menuItem);
});
menu.style.left = x + "px";
menu.style.top = y + "px";
menu.style.display = "block";
}
function hideFolderManagerContextMenu() {
const menu = document.getElementById("folderManagerContextMenu");
if (menu) {
menu.style.display = "none";
}
}
// Context menu handler for folder tree and breadcrumb items.
function folderManagerContextMenuHandler(e) {
e.preventDefault();
e.stopPropagation();
// Get the closest folder element (either from the tree or breadcrumb).
const target = e.target.closest(".folder-option, .breadcrumb-link");
if (!target) return;
const folder = target.getAttribute("data-folder");
if (!folder) return;
// Update current folder and highlight the selected element.
window.currentFolder = folder;
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
target.classList.add("selected");
// Build context menu items.
const menuItems = [
{
label: "Create Folder",
action: () => {
document.getElementById("createFolderModal").style.display = "block";
document.getElementById("newFolderName").focus();
}
},
{
label: "Rename Folder",
action: () => { openRenameFolderModal(); }
},
{
label: "Delete Folder",
action: () => { openDeleteFolderModal(); }
}
];
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
}
// Bind contextmenu events to folder tree and breadcrumb elements.
function bindFolderManagerContextMenu() {
// Bind context menu to folder tree container.
const container = document.getElementById("folderTreeContainer");
if (container) {
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
}
// Bind context menu to breadcrumb links.
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
breadcrumbNodes.forEach(node => {
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
node.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
});
}
// Hide context menu when clicking elsewhere.
document.addEventListener("click", function () {
hideFolderManagerContextMenu();
});
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("keydown", function(e) {
// Skip if the user is typing in an input, textarea, or contentEditable element.
const tag = e.target.tagName.toLowerCase();
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
return;
}
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
// Ensure a folder is selected and it isn't the root folder.
if (window.currentFolder && window.currentFolder !== "root") {
// Prevent default (avoid navigating back on macOS).
e.preventDefault();
// Call your existing folder delete function.
openDeleteFolderModal();
}
}
});
});
// Call this binding function after rendering the folder tree and breadcrumbs.
bindFolderManagerContextMenu();

View File

@@ -302,8 +302,8 @@
</div>
</div>
</div>
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button>
<!-- Download Zip Modal -->
<div id="downloadZipModal" class="modal" style="display:none;">
<div class="modal-content">

View File

@@ -1883,4 +1883,23 @@ body.dark-mode .drop-hover {
#restoreFilesList li label {
margin-left: 8px !important;
}
body.dark-mode #fileContextMenu {
background-color: #2c2c2c !important;
border: 1px solid #555 !important;
color: #e0e0e0 !important;
}
body.dark-mode #fileContextMenu div {
color: #e0e0e0 !important;
}
#folderContextMenu {
font-family: Arial, sans-serif;
font-size: 14px;
}
body.dark-mode #folderContextMenu {
background-color: #2c2c2c;
border-color: #555;
color: #e0e0e0;
}