Authentication & Initialization Changes plus File & Fold Manager Enhancements
This commit is contained in:
228
auth.js
228
auth.js
@@ -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() {
|
||||
|
||||
31
domUtils.js
31
domUtils.js
@@ -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
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;
|
||||
?>
|
||||
236
fileManager.js
236
fileManager.js
@@ -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();
|
||||
};
|
||||
})();
|
||||
158
folderManager.js
158
folderManager.js
@@ -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();
|
||||
@@ -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">
|
||||
|
||||
19
styles.css
19
styles.css
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user