Authentication & Initialization Changes plus File & Fold Manager Enhancements
This commit is contained in:
142
auth.js
142
auth.js
@@ -3,22 +3,23 @@ import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmM
|
|||||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||||
import { loadFolderTree } from './folderManager.js';
|
import { loadFolderTree } from './folderManager.js';
|
||||||
|
|
||||||
function initAuth() {
|
/**
|
||||||
// First, check if the user is already authenticated.
|
* Updates the select element to reflect the stored items-per-page value.
|
||||||
checkAuthentication(false).then(data => {
|
*/
|
||||||
if (data.setup) {
|
function updateItemsPerPageSelect() {
|
||||||
window.setupMode = true;
|
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||||
showToast("Setup mode: No users found. Please add an admin user.");
|
if (selectElem) {
|
||||||
toggleVisibility("loginForm", false);
|
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||||
toggleVisibility("mainOperations", false);
|
selectElem.value = stored;
|
||||||
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.
|
/**
|
||||||
|
* 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("loginForm", false);
|
||||||
toggleVisibility("mainOperations", true);
|
toggleVisibility("mainOperations", true);
|
||||||
toggleVisibility("uploadFileForm", true);
|
toggleVisibility("uploadFileForm", true);
|
||||||
@@ -27,19 +28,19 @@ function initAuth() {
|
|||||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
// If admin, show admin-only buttons.
|
|
||||||
|
// If admin, show admin-only buttons; otherwise hide them.
|
||||||
if (data.isAdmin) {
|
if (data.isAdmin) {
|
||||||
const addUserBtn = document.getElementById("addUserBtn");
|
const addUserBtn = document.getElementById("addUserBtn");
|
||||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||||
if (addUserBtn) addUserBtn.style.display = "block";
|
if (addUserBtn) addUserBtn.style.display = "block";
|
||||||
if (removeUserBtn) removeUserBtn.style.display = "block";
|
if (removeUserBtn) removeUserBtn.style.display = "block";
|
||||||
// Create and show the restore button.
|
|
||||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (!restoreBtn) {
|
if (!restoreBtn) {
|
||||||
restoreBtn = document.createElement("button");
|
restoreBtn = document.createElement("button");
|
||||||
restoreBtn.id = "restoreFilesBtn";
|
restoreBtn.id = "restoreFilesBtn";
|
||||||
restoreBtn.classList.add("btn", "btn-warning");
|
restoreBtn.classList.add("btn", "btn-warning");
|
||||||
// Use a material icon.
|
// Using a material icon for restore.
|
||||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||||
const headerButtons = document.querySelector(".header-buttons");
|
const headerButtons = document.querySelector(".header-buttons");
|
||||||
if (headerButtons) {
|
if (headerButtons) {
|
||||||
@@ -57,24 +58,55 @@ function initAuth() {
|
|||||||
if (addUserBtn) addUserBtn.style.display = "none";
|
if (addUserBtn) addUserBtn.style.display = "none";
|
||||||
if (removeUserBtn) removeUserBtn.style.display = "none";
|
if (removeUserBtn) removeUserBtn.style.display = "none";
|
||||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||||
if (restoreBtn) {
|
if (restoreBtn) restoreBtn.style.display = "none";
|
||||||
restoreBtn.style.display = "none";
|
|
||||||
}
|
}
|
||||||
|
updateItemsPerPageSelect();
|
||||||
}
|
}
|
||||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
|
||||||
if (selectElem) {
|
/**
|
||||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
* Checks the user's authentication state and updates the UI accordingly.
|
||||||
selectElem.value = stored;
|
* 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 {
|
} else {
|
||||||
|
if (showLoginToast) showToast("Please log in to continue.");
|
||||||
toggleVisibility("loginForm", true);
|
toggleVisibility("loginForm", true);
|
||||||
attachEnterKeyListener("loginModal", "loginBtn");
|
|
||||||
toggleVisibility("mainOperations", false);
|
toggleVisibility("mainOperations", false);
|
||||||
toggleVisibility("uploadFileForm", false);
|
toggleVisibility("uploadFileForm", false);
|
||||||
toggleVisibility("fileListContainer", false);
|
toggleVisibility("fileListContainer", false);
|
||||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}).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);
|
console.error("Error checking authentication:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,7 +115,6 @@ function initAuth() {
|
|||||||
if (authForm) {
|
if (authForm) {
|
||||||
authForm.addEventListener("submit", function (event) {
|
authForm.addEventListener("submit", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Get the "Remember me" checkbox value.
|
|
||||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||||
? document.getElementById("rememberMeCheckbox").checked
|
? document.getElementById("rememberMeCheckbox").checked
|
||||||
: false;
|
: false;
|
||||||
@@ -137,10 +168,8 @@ function initAuth() {
|
|||||||
});
|
});
|
||||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||||
const newUsername = document.getElementById("newUsername").value.trim();
|
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 newPassword = document.getElementById("addUserPassword").value.trim();
|
||||||
const isAdmin = document.getElementById("isAdmin").checked;
|
const isAdmin = document.getElementById("isAdmin").checked;
|
||||||
console.log("newUsername:", newUsername, "newPassword:", newPassword);
|
|
||||||
if (!newUsername || !newPassword) {
|
if (!newUsername || !newPassword) {
|
||||||
showToast("Username and password are required!");
|
showToast("Username and password are required!");
|
||||||
return;
|
return;
|
||||||
@@ -163,6 +192,7 @@ function initAuth() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast("User added successfully!");
|
showToast("User added successfully!");
|
||||||
closeAddUserModal();
|
closeAddUserModal();
|
||||||
|
// Re-check auth state to update the UI after adding a user.
|
||||||
checkAuthentication(false);
|
checkAuthentication(false);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error: " + (data.error || "Could not add user"));
|
showToast("Error: " + (data.error || "Could not add user"));
|
||||||
@@ -187,14 +217,10 @@ function initAuth() {
|
|||||||
showToast("Please select a user to remove.");
|
showToast("Please select a user to remove.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await the confirmation result from your custom modal helper.
|
|
||||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with deletion...
|
|
||||||
fetch("removeUser.php", {
|
fetch("removeUser.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -222,35 +248,27 @@ function initAuth() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||||
// Show the Change Password modal.
|
|
||||||
document.getElementById("changePasswordModal").style.display = "block";
|
document.getElementById("changePasswordModal").style.display = "block";
|
||||||
document.getElementById("oldPassword").focus();
|
document.getElementById("oldPassword").focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||||
// Hide the Change Password modal.
|
|
||||||
document.getElementById("changePasswordModal").style.display = "none";
|
document.getElementById("changePasswordModal").style.display = "none";
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
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();
|
const confirmPassword = document.getElementById("confirmPassword").value.trim();
|
||||||
|
|
||||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||||
showToast("Please fill in all fields.");
|
showToast("Please fill in all fields.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
showToast("New passwords do not match.");
|
showToast("New passwords do not match.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the data to send.
|
|
||||||
const data = { oldPassword, newPassword, confirmPassword };
|
const data = { oldPassword, newPassword, confirmPassword };
|
||||||
|
|
||||||
// Send request to changePassword.php.
|
|
||||||
fetch("changePassword.php", {
|
fetch("changePassword.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -264,7 +282,6 @@ function initAuth() {
|
|||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
showToast(result.success);
|
showToast(result.success);
|
||||||
// Clear form fields and close modal.
|
|
||||||
document.getElementById("oldPassword").value = "";
|
document.getElementById("oldPassword").value = "";
|
||||||
document.getElementById("newPassword").value = "";
|
document.getElementById("newPassword").value = "";
|
||||||
document.getElementById("confirmPassword").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) {
|
window.changeItemsPerPage = function (value) {
|
||||||
localStorage.setItem("itemsPerPage", value);
|
localStorage.setItem("itemsPerPage", value);
|
||||||
const folder = window.currentFolder || "root";
|
const folder = window.currentFolder || "root";
|
||||||
@@ -322,11 +306,7 @@ window.changeItemsPerPage = function (value) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
updateItemsPerPageSelect();
|
||||||
if (selectElem) {
|
|
||||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
|
||||||
selectElem.value = stored;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetUserForm() {
|
function resetUserForm() {
|
||||||
|
|||||||
31
domUtils.js
31
domUtils.js
@@ -28,35 +28,39 @@ export function toggleAllCheckboxes(masterCheckbox) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateFileActionButtons() {
|
export function updateFileActionButtons() {
|
||||||
const fileListContainer = document.getElementById("fileList");
|
|
||||||
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
||||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||||
const copyBtn = document.getElementById("copySelectedBtn");
|
const copyBtn = document.getElementById("copySelectedBtn");
|
||||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||||
const zipBtn = document.getElementById("downloadZipBtn");
|
const zipBtn = document.getElementById("downloadZipBtn");
|
||||||
|
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||||
|
|
||||||
if (fileCheckboxes.length === 0) {
|
if (fileCheckboxes.length === 0) {
|
||||||
if (copyBtn) copyBtn.style.display = "none";
|
if (copyBtn) copyBtn.style.display = "none";
|
||||||
if (moveBtn) moveBtn.style.display = "none";
|
if (moveBtn) moveBtn.style.display = "none";
|
||||||
if (deleteBtn) deleteBtn.style.display = "none";
|
if (deleteBtn) deleteBtn.style.display = "none";
|
||||||
if (zipBtn) zipBtn.style.display = "none";
|
if (zipBtn) zipBtn.style.display = "none";
|
||||||
|
if (extractZipBtn) extractZipBtn.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
if (copyBtn) copyBtn.style.display = "inline-block";
|
if (copyBtn) copyBtn.style.display = "inline-block";
|
||||||
if (moveBtn) moveBtn.style.display = "inline-block";
|
if (moveBtn) moveBtn.style.display = "inline-block";
|
||||||
if (deleteBtn) deleteBtn.style.display = "inline-block";
|
if (deleteBtn) deleteBtn.style.display = "inline-block";
|
||||||
if (zipBtn) zipBtn.style.display = "inline-block";
|
if (zipBtn) zipBtn.style.display = "inline-block";
|
||||||
|
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
|
||||||
|
|
||||||
if (selectedCheckboxes.length > 0) {
|
const anySelected = selectedCheckboxes.length > 0;
|
||||||
if (copyBtn) copyBtn.disabled = false;
|
if (copyBtn) copyBtn.disabled = !anySelected;
|
||||||
if (moveBtn) moveBtn.disabled = false;
|
if (moveBtn) moveBtn.disabled = !anySelected;
|
||||||
if (deleteBtn) deleteBtn.disabled = false;
|
if (deleteBtn) deleteBtn.disabled = !anySelected;
|
||||||
if (zipBtn) zipBtn.disabled = false;
|
if (zipBtn) zipBtn.disabled = !anySelected;
|
||||||
} else {
|
|
||||||
if (copyBtn) copyBtn.disabled = true;
|
if (extractZipBtn) {
|
||||||
if (moveBtn) moveBtn.disabled = true;
|
// Enable only if at least one selected file ends with .zip (case-insensitive).
|
||||||
if (deleteBtn) deleteBtn.disabled = true;
|
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
|
||||||
if (zipBtn) zipBtn.disabled = true;
|
chk.value.toLowerCase().endsWith(".zip")
|
||||||
|
);
|
||||||
|
extractZipBtn.disabled = !anyZipSelected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +314,10 @@ export function previewFile(fileUrl, fileName) {
|
|||||||
export function attachEnterKeyListener(modalId, buttonId) {
|
export function attachEnterKeyListener(modalId, buttonId) {
|
||||||
const modal = document.getElementById(modalId);
|
const modal = document.getElementById(modalId);
|
||||||
if (modal) {
|
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") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const btn = document.getElementById(buttonId);
|
const btn = document.getElementById(buttonId);
|
||||||
|
|||||||
146
extractZip.php
Normal file
146
extractZip.php
Normal 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;
|
||||||
|
?>
|
||||||
234
fileManager.js
234
fileManager.js
@@ -9,6 +9,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
updateRowHighlight,
|
updateRowHighlight,
|
||||||
toggleRowSelection,
|
toggleRowSelection,
|
||||||
|
attachEnterKeyListener,
|
||||||
previewFile as originalPreviewFile
|
previewFile as originalPreviewFile
|
||||||
} from './domUtils.js';
|
} from './domUtils.js';
|
||||||
|
|
||||||
@@ -661,6 +662,7 @@ export function handleDeleteSelected(e) {
|
|||||||
document.getElementById("deleteFilesMessage").textContent =
|
document.getElementById("deleteFilesMessage").textContent =
|
||||||
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
|
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
|
||||||
document.getElementById("deleteFilesModal").style.display = "block";
|
document.getElementById("deleteFilesModal").style.display = "block";
|
||||||
|
attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
@@ -671,6 +673,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
window.filesToDelete = [];
|
window.filesToDelete = [];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = document.getElementById("confirmDeleteFiles");
|
const confirmDelete = document.getElementById("confirmDeleteFiles");
|
||||||
if (confirmDelete) {
|
if (confirmDelete) {
|
||||||
confirmDelete.addEventListener("click", function () {
|
confirmDelete.addEventListener("click", function () {
|
||||||
@@ -700,7 +703,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
attachEnterKeyListener("downloadZipModal", "confirmDownloadZip");
|
||||||
export function handleDownloadZipSelected(e) {
|
export function handleDownloadZipSelected(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
@@ -711,6 +714,64 @@ export function handleDownloadZipSelected(e) {
|
|||||||
}
|
}
|
||||||
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
|
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
|
||||||
document.getElementById("downloadZipModal").style.display = "block";
|
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 () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
@@ -720,6 +781,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
document.getElementById("downloadZipModal").style.display = "none";
|
document.getElementById("downloadZipModal").style.display = "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
||||||
if (confirmDownloadZip) {
|
if (confirmDownloadZip) {
|
||||||
confirmDownloadZip.addEventListener("click", function () {
|
confirmDownloadZip.addEventListener("click", function () {
|
||||||
@@ -1035,7 +1097,7 @@ export function editFile(fileName, folder) {
|
|||||||
fetch(fileUrl, { method: "HEAD" })
|
fetch(fileUrl, { method: "HEAD" })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const contentLength = response.headers.get("Content-Length");
|
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.");
|
showToast("This file is larger than 10 MB and cannot be edited in the browser.");
|
||||||
throw new Error("File too large.");
|
throw new Error("File too large.");
|
||||||
}
|
}
|
||||||
@@ -1196,8 +1258,13 @@ export function initFileActions() {
|
|||||||
downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true));
|
downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true));
|
||||||
document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected);
|
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) {
|
export function renameFile(oldName, folder) {
|
||||||
window.fileToRename = oldName;
|
window.fileToRename = oldName;
|
||||||
window.fileFolder = folder || window.currentFolder || "root";
|
window.fileFolder = folder || window.currentFolder || "root";
|
||||||
@@ -1223,6 +1290,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.getElementById("newFileName").value = "";
|
document.getElementById("newFileName").value = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitBtn = document.getElementById("submitRenameFile");
|
const submitBtn = document.getElementById("submitRenameFile");
|
||||||
if (submitBtn) {
|
if (submitBtn) {
|
||||||
submitBtn.addEventListener("click", function () {
|
submitBtn.addEventListener("click", function () {
|
||||||
@@ -1284,3 +1352,163 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
el.addEventListener("drop", folderDropHandler);
|
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();
|
||||||
|
};
|
||||||
|
})();
|
||||||
156
folderManager.js
156
folderManager.js
@@ -1,7 +1,7 @@
|
|||||||
// folderManager.js
|
// folderManager.js
|
||||||
|
|
||||||
import { loadFileList } from './fileManager.js';
|
import { loadFileList } from './fileManager.js';
|
||||||
import { showToast, escapeHTML } from './domUtils.js';
|
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||||
|
|
||||||
// ----------------------
|
// ----------------------
|
||||||
// Helper Functions (Data/State)
|
// Helper Functions (Data/State)
|
||||||
@@ -90,7 +90,6 @@ function bindBreadcrumbEvents() {
|
|||||||
link.addEventListener("click", function (e) {
|
link.addEventListener("click", function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
let folder = this.getAttribute("data-folder");
|
let folder = this.getAttribute("data-folder");
|
||||||
console.log("Breadcrumb clicked, folder:", folder);
|
|
||||||
window.currentFolder = folder;
|
window.currentFolder = folder;
|
||||||
localStorage.setItem("lastOpenedFolder", folder);
|
localStorage.setItem("lastOpenedFolder", folder);
|
||||||
const titleEl = document.getElementById("fileListTitle");
|
const titleEl = document.getElementById("fileListTitle");
|
||||||
@@ -447,6 +446,7 @@ export function loadFolderList(selectedFolder) {
|
|||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||||
|
|
||||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||||
|
|
||||||
function openRenameFolderModal() {
|
function openRenameFolderModal() {
|
||||||
@@ -470,7 +470,7 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function
|
|||||||
document.getElementById("renameFolderModal").style.display = "none";
|
document.getElementById("renameFolderModal").style.display = "none";
|
||||||
document.getElementById("newRenameFolderName").value = "";
|
document.getElementById("newRenameFolderName").value = "";
|
||||||
});
|
});
|
||||||
|
attachEnterKeyListener("renameFolderModal", "submitRenameFolder");
|
||||||
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
|
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const selectedFolder = window.currentFolder || "root";
|
const selectedFolder = window.currentFolder || "root";
|
||||||
@@ -527,7 +527,7 @@ function openDeleteFolderModal() {
|
|||||||
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
|
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
|
||||||
document.getElementById("deleteFolderModal").style.display = "none";
|
document.getElementById("deleteFolderModal").style.display = "none";
|
||||||
});
|
});
|
||||||
|
attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
|
||||||
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
||||||
const selectedFolder = window.currentFolder || "root";
|
const selectedFolder = window.currentFolder || "root";
|
||||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
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("createFolderModal").style.display = "none";
|
||||||
document.getElementById("newFolderName").value = "";
|
document.getElementById("newFolderName").value = "";
|
||||||
});
|
});
|
||||||
|
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||||
if (!folderInput) {
|
if (!folderInput) {
|
||||||
@@ -607,3 +607,149 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
|||||||
document.getElementById("createFolderModal").style.display = "none";
|
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();
|
||||||
@@ -302,8 +302,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
|
<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 -->
|
<!-- Download Zip Modal -->
|
||||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
19
styles.css
19
styles.css
@@ -1884,3 +1884,22 @@ body.dark-mode .drop-hover {
|
|||||||
#restoreFilesList li label {
|
#restoreFilesList li label {
|
||||||
margin-left: 8px !important;
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user