new features and refactor
This commit is contained in:
54
README.md
54
README.md
@@ -1,8 +1,46 @@
|
||||
|
||||
|
||||
|
||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/main-screen.png" alt="main screen">
|
||||
|
||||
**Changes 3/7/2025:***
|
||||
|
||||
- Module Separation & ES6 Conversion
|
||||
- networkUtils.js: For handling HTTP requests.
|
||||
- domUtils.js: For DOM manipulation functions (e.g. toggleVisibility, escapeHTML, toggleAllCheckboxes, and file action button updates).
|
||||
- fileManager.js: For file operations, rendering the file list, sorting, editing, renaming, and pagination.
|
||||
- folderManager.js: For folder-related operations (loading folder lists, renaming/deleting folders, etc.).
|
||||
- upload.js: For handling file uploads and progress display.
|
||||
- auth.js: For authentication and user management.
|
||||
- Converted all modules to ES6
|
||||
|
||||
- File List Rendering & Pagination in fileManager.js
|
||||
- Implemented Pagination
|
||||
- Added global settings (window.itemsPerPage and window.currentPage) with defaults (10 items per page).
|
||||
- Modified renderFileTable() to calculate the current slice of files and render pagination controls (with “Prev”/“Next” buttons and an items-per-page selector).
|
||||
- Reworked Sorting
|
||||
- updated sortFiles() to re-render the table on sorting.
|
||||
- Implemented sorting for non-date columns by converting strings to lowercase.
|
||||
- Date sorting improvements
|
||||
|
||||
- File Upload Enhancements in upload.js
|
||||
- Maintained individual progress tracking for the first 10 files while still uploading all selected files.
|
||||
- Implemented logic to refresh the file list instantly after uploads finish.
|
||||
- Configured the progress list to remain visible for 10 seconds after the file list refresh so users can verify the upload status.
|
||||
- Ensured that after refreshing the file list, event listeners for actions (delete, copy, move) are reattached.
|
||||
- File upload error handling and display
|
||||
|
||||
- File Action Buttons & Checkbox Handling (domUtils.js and fileManager.js)
|
||||
- Rewrote the updateFileActionButtons()
|
||||
- Removed duplicate or conflicting logic from renderFileTable() and initFileActions() that previously managed button visibility.
|
||||
- Adjusted toggleAllCheckboxes() and toggleDeleteButton() so they call updateFileActionButtons() to maintain a single source of truth.
|
||||
|
||||
- Rename Functionality
|
||||
- Updated the Actions column in the file table to always include a “Rename” button for each file.
|
||||
- Implemented renameFile()
|
||||
|
||||
- Responsive Behavior & Additional UI Tweaks
|
||||
- Added CSS media queries to hide less critical columns (Date Modified, Upload Date, File Size, Uploader) on smaller screens.
|
||||
- Adjusted margins on file preview images and file icons.
|
||||
- Improved header centering and button styling.
|
||||
|
||||
**Changes 3/4/2025:**
|
||||
Copy & Move functionality added
|
||||
Header Layout
|
||||
@@ -11,7 +49,6 @@
|
||||
CSS Consolidation
|
||||
assets folder
|
||||
|
||||
|
||||
**Changes 3/3/2025:**
|
||||
folder management added
|
||||
some refactoring
|
||||
@@ -106,20 +143,14 @@ This project is a lightweight, secure web application for uploading, editing, an
|
||||
This multi-file uploader with editing and user management is ideal for scenarios involving document management, image galleries, firmware updates, and more.
|
||||
|
||||
---
|
||||
|
||||
- **Login Page**
|
||||
|
||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/login-page.png" alt="login page" width="600">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
based off of:
|
||||
https://github.com/sensboston/uploader
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Apache2, configured, up and running
|
||||
@@ -127,4 +158,3 @@ https://github.com/sensboston/uploader
|
||||
- Required PHP extensions: `php-json`, `php-curl`
|
||||
|
||||
...........
|
||||
|
||||
|
||||
168
auth.js
168
auth.js
@@ -1,15 +1,18 @@
|
||||
// auth.js
|
||||
import { sendRequest, toggleVisibility } from './utils.js';
|
||||
|
||||
let setupMode = false; // Declare setupMode here
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility } from './domUtils.js';
|
||||
// Import loadFileList from fileManager.js to refresh the file list upon login.
|
||||
import { loadFileList } from './fileManager.js';
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Hide file list and upload form on load.
|
||||
toggleVisibility("fileListContainer", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
|
||||
checkAuthentication();
|
||||
export function initAuth() {
|
||||
// On initial load, show the login form and hide the main operations.
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
// Ensure header buttons are hidden.
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
|
||||
// Set up the authentication form listener.
|
||||
document.getElementById("authForm").addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
@@ -22,58 +25,179 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
console.log("Login response:", data);
|
||||
if (data.success) {
|
||||
console.log("Login successful.");
|
||||
// On successful login, hide the login form and show main operations.
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
checkAuthentication(); // Recheck authentication to update UI.
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
// Refresh the file list immediately using the current folder.
|
||||
loadFileList(window.currentFolder || "root");
|
||||
// Optionally, you can also call checkAuthentication() to update UI further.
|
||||
checkAuthentication();
|
||||
} else {
|
||||
alert("Login failed: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error logging in:", error));
|
||||
});
|
||||
});
|
||||
|
||||
// Set up the logout button.
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("logout.php", { method: "POST" })
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(error => console.error("Logout error:", error));
|
||||
});
|
||||
|
||||
// Set up Add User functionality.
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
});
|
||||
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
if (!newUsername || !newPassword) {
|
||||
alert("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
let url = "addUser.php";
|
||||
if (window.setupMode) {
|
||||
url += "?setup=1";
|
||||
}
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error adding user:", error));
|
||||
});
|
||||
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", function () {
|
||||
closeAddUserModal();
|
||||
});
|
||||
|
||||
// Set up Remove User functionality.
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
if (!usernameToRemove) {
|
||||
alert("Please select a user to remove.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
|
||||
return;
|
||||
}
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("User removed successfully!");
|
||||
closeRemoveUserModal();
|
||||
loadUserList();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error removing user:", error));
|
||||
});
|
||||
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
|
||||
closeRemoveUserModal();
|
||||
});
|
||||
}
|
||||
|
||||
export function checkAuthentication() {
|
||||
sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
console.log("Authentication check:", data);
|
||||
if (data.setup) {
|
||||
setupMode = true;
|
||||
// In setup mode, hide all sections except the Add User modal.
|
||||
window.setupMode = true;
|
||||
// In setup mode, hide login and main operations; show Add User modal.
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
return;
|
||||
} else {
|
||||
setupMode = false;
|
||||
window.setupMode = false;
|
||||
}
|
||||
if (data.authenticated) {
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
if (typeof loadFileList === "function") {
|
||||
loadFileList();
|
||||
}
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
} else {
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error checking authentication:", error));
|
||||
}
|
||||
window.checkAuthentication = checkAuthentication;
|
||||
|
||||
// Helper functions for the Add User modal.
|
||||
// Helper functions for auth modals.
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
toggleVisibility("addUserModal", false);
|
||||
resetUserForm();
|
||||
}
|
||||
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
function closeRemoveUserModal() {
|
||||
toggleVisibility("removeUserModal", false);
|
||||
document.getElementById("removeUsernameSelect").innerHTML = "";
|
||||
}
|
||||
|
||||
function loadUserList() {
|
||||
fetch("getUsers.php")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const users = Array.isArray(data) ? data : (data.users || []);
|
||||
if (!users || !Array.isArray(users)) {
|
||||
console.error("Invalid users data:", data);
|
||||
return;
|
||||
}
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
selectElem.innerHTML = "";
|
||||
users.forEach(user => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.username;
|
||||
option.textContent = user.username;
|
||||
selectElem.appendChild(option);
|
||||
});
|
||||
if (selectElem.options.length === 0) {
|
||||
alert("No other users found to remove.");
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading user list:", error));
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
// displayFileList.js
|
||||
|
||||
import { sendRequest, toggleVisibility } from './utils.js';
|
||||
|
||||
let fileData = [];
|
||||
let sortOrder = { column: "uploaded", ascending: false };
|
||||
export let currentFolder = "root"; // Global current folder
|
||||
|
||||
export function loadFileList() {
|
||||
sendRequest("checkAuth.php")
|
||||
.then(authData => {
|
||||
if (!authData.authenticated) {
|
||||
console.warn("User not authenticated, hiding file list.");
|
||||
toggleVisibility("fileListContainer", false);
|
||||
return;
|
||||
}
|
||||
toggleVisibility("fileListContainer", true);
|
||||
return sendRequest("getFileList.php?folder=" + encodeURIComponent(currentFolder));
|
||||
})
|
||||
.then(data => {
|
||||
if (!data) return;
|
||||
if (data.error) {
|
||||
document.getElementById("fileList").innerHTML = `<p style="color:red;">Error: ${data.error}</p>`;
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data.files)) {
|
||||
console.error("Unexpected response format:", data);
|
||||
return;
|
||||
}
|
||||
fileData = data.files;
|
||||
//sortFiles("uploaded", false);
|
||||
})
|
||||
.catch(error => console.error("Error loading file list:", error));
|
||||
}
|
||||
|
||||
export function toggleDeleteButton() {
|
||||
const selectedFiles = document.querySelectorAll(".file-checkbox:checked");
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
const copyBtn = document.getElementById("copySelectedBtn");
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
const disabled = selectedFiles.length === 0;
|
||||
deleteBtn.disabled = disabled;
|
||||
if (copyBtn) copyBtn.disabled = disabled;
|
||||
if (moveBtn) moveBtn.disabled = disabled;
|
||||
}
|
||||
|
||||
export function toggleAllCheckboxes(source) {
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||
checkboxes.forEach(checkbox => checkbox.checked = source.checked);
|
||||
toggleDeleteButton();
|
||||
}
|
||||
|
||||
export function deleteSelectedFiles() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll(".file-checkbox:checked"))
|
||||
.map(checkbox => checkbox.value);
|
||||
if (selectedFiles.length === 0) {
|
||||
alert("No files selected for deletion.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete the selected files?")) {
|
||||
return;
|
||||
}
|
||||
sendRequest("deleteFiles.php", "POST", { files: selectedFiles })
|
||||
.then(result => {
|
||||
alert(result.success || result.error);
|
||||
loadFileList();
|
||||
})
|
||||
.catch(error => console.error("Error deleting files:", error));
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
loadFileList();
|
||||
loadCopyMoveFolderList();
|
||||
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener("click", deleteSelectedFiles);
|
||||
}
|
||||
|
||||
const copyBtn = document.getElementById("copySelectedBtn");
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener("click", copySelectedFiles);
|
||||
}
|
||||
if (moveBtn) {
|
||||
moveBtn.addEventListener("click", moveSelectedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ===== NEW CODE: Copy & Move Functions =====
|
||||
|
||||
// Copy selected files to a target folder
|
||||
export function copySelectedFiles() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll(".file-checkbox:checked"))
|
||||
.map(checkbox => checkbox.value);
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (selectedFiles.length === 0) {
|
||||
alert("Please select at least one file to copy.");
|
||||
return;
|
||||
}
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder.");
|
||||
return;
|
||||
}
|
||||
if (currentFolder === targetFolder) {
|
||||
alert("Cannot copy files to the same folder.");
|
||||
return;
|
||||
}
|
||||
// Send the correct keys
|
||||
sendRequest("copyFiles.php", "POST", {
|
||||
source: currentFolder,
|
||||
destination: targetFolder,
|
||||
files: selectedFiles
|
||||
})
|
||||
.then(result => {
|
||||
alert(result.success || result.error);
|
||||
loadFileList();
|
||||
})
|
||||
.catch(error => console.error("Error copying files:", error));
|
||||
}
|
||||
|
||||
export function moveSelectedFiles() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll(".file-checkbox:checked"))
|
||||
.map(checkbox => checkbox.value);
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (selectedFiles.length === 0) {
|
||||
alert("Please select at least one file to move.");
|
||||
return;
|
||||
}
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder.");
|
||||
return;
|
||||
}
|
||||
if (currentFolder === targetFolder) {
|
||||
alert("Cannot move files to the same folder.");
|
||||
return;
|
||||
}
|
||||
console.log("Payload:", {
|
||||
source: currentFolder,
|
||||
destination: targetFolder,
|
||||
files: selectedFiles
|
||||
});
|
||||
sendRequest("moveFiles.php", "POST", {
|
||||
source: currentFolder,
|
||||
destination: targetFolder,
|
||||
files: selectedFiles
|
||||
})
|
||||
.then(result => {
|
||||
alert(result.success || result.error);
|
||||
loadFileList();
|
||||
})
|
||||
.catch(error => console.error("Error moving files:", error));
|
||||
}
|
||||
|
||||
|
||||
// Populate the Copy/Move folder dropdown
|
||||
export function loadCopyMoveFolderList() {
|
||||
$.get('getFolderList.php', function (response) {
|
||||
const folderSelect = $('#copyMoveFolderSelect');
|
||||
folderSelect.empty();
|
||||
// Always add a "Root" option as the default.
|
||||
folderSelect.append($('<option>', { value: "root", text: "(Root)" }));
|
||||
if (Array.isArray(response) && response.length > 0) {
|
||||
response.forEach(function (folder) {
|
||||
folderSelect.append($('<option>', {
|
||||
value: folder,
|
||||
text: folder
|
||||
}));
|
||||
});
|
||||
}
|
||||
}, 'json');
|
||||
}
|
||||
|
||||
// Attach functions to window for inline onclick support
|
||||
window.toggleDeleteButton = toggleDeleteButton;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.deleteSelectedFiles = deleteSelectedFiles;
|
||||
window.loadFileList = loadFileList;
|
||||
window.copySelectedFiles = copySelectedFiles;
|
||||
window.moveSelectedFiles = moveSelectedFiles;
|
||||
70
domUtils.js
Normal file
70
domUtils.js
Normal file
@@ -0,0 +1,70 @@
|
||||
// domUtils.js
|
||||
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.display = shouldShow ? "block" : "none";
|
||||
} else {
|
||||
console.error(`Element with id "${elementId}" not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHTML(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
// Toggle all checkboxes (assumes checkboxes have class 'file-checkbox')
|
||||
export function toggleAllCheckboxes(masterCheckbox) {
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox");
|
||||
checkboxes.forEach(chk => {
|
||||
chk.checked = masterCheckbox.checked;
|
||||
});
|
||||
updateFileActionButtons(); // call the updated function
|
||||
}
|
||||
|
||||
// This updateFileActionButtons function checks for checkboxes inside the file list container.
|
||||
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 folderDropdown = document.getElementById("copyMoveFolderSelect");
|
||||
|
||||
// Hide the buttons and dropdown if no files exist.
|
||||
if (fileCheckboxes.length === 0) {
|
||||
copyBtn.style.display = "none";
|
||||
moveBtn.style.display = "none";
|
||||
deleteBtn.style.display = "none";
|
||||
folderDropdown.style.display = "none";
|
||||
} else {
|
||||
// Otherwise, show the buttons and dropdown.
|
||||
copyBtn.style.display = "inline-block";
|
||||
moveBtn.style.display = "inline-block";
|
||||
deleteBtn.style.display = "inline-block";
|
||||
folderDropdown.style.display = "inline-block";
|
||||
|
||||
// Enable the buttons if at least one file is selected; otherwise disable.
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
copyBtn.disabled = false;
|
||||
moveBtn.disabled = false;
|
||||
deleteBtn.disabled = false;
|
||||
} else {
|
||||
copyBtn.disabled = true;
|
||||
moveBtn.disabled = true;
|
||||
deleteBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
483
fileManager.js
Normal file
483
fileManager.js
Normal file
@@ -0,0 +1,483 @@
|
||||
// fileManager.js
|
||||
import { escapeHTML, updateFileActionButtons } from './domUtils.js';
|
||||
|
||||
export let fileData = [];
|
||||
export let sortOrder = { column: "uploaded", ascending: true };
|
||||
|
||||
// Global pagination defaults
|
||||
window.itemsPerPage = window.itemsPerPage || 10;
|
||||
window.currentPage = window.currentPage || 1;
|
||||
|
||||
// Helper to parse date strings in the "m/d/y h:iA" format into a timestamp.
|
||||
// Custom date parser (expected format: "MM/DD/YY hh:mma", e.g., "03/07/25 01:01AM")
|
||||
function parseCustomDate(dateStr) {
|
||||
// Normalize whitespace (replace one or more whitespace characters with a single space)
|
||||
dateStr = dateStr.replace(/\s+/g, " ").trim();
|
||||
|
||||
// Expected format: "MM/DD/YY hh:mma" (e.g., "03/07/25 01:01AM")
|
||||
const parts = dateStr.split(" ");
|
||||
if (parts.length !== 2) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
const datePart = parts[0]; // e.g., "03/07/25"
|
||||
const timePart = parts[1]; // e.g., "01:01AM"
|
||||
|
||||
const dateComponents = datePart.split("/");
|
||||
if (dateComponents.length !== 3) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
|
||||
let month = parseInt(dateComponents[0], 10);
|
||||
let day = parseInt(dateComponents[1], 10);
|
||||
let year = parseInt(dateComponents[2], 10);
|
||||
if (year < 100) {
|
||||
year += 2000;
|
||||
}
|
||||
|
||||
// Expect timePart in format hh:mma, e.g., "01:01AM"
|
||||
const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i;
|
||||
const match = timePart.match(timeRegex);
|
||||
if (!match) {
|
||||
return new Date(dateStr).getTime();
|
||||
}
|
||||
|
||||
let hour = parseInt(match[1], 10);
|
||||
const minute = parseInt(match[2], 10);
|
||||
const period = match[3].toUpperCase();
|
||||
|
||||
if (period === "PM" && hour !== 12) {
|
||||
hour += 12;
|
||||
}
|
||||
if (period === "AM" && hour === 12) {
|
||||
hour = 0;
|
||||
}
|
||||
|
||||
return new Date(year, month - 1, day, hour, minute).getTime();
|
||||
}
|
||||
|
||||
// Determines if a file is editable based on its extension.
|
||||
export function canEditFile(fileName) {
|
||||
const allowedExtensions = ["txt", "html", "htm", "php", "css", "js", "json", "xml", "md", "py", "ini", "csv", "log", "conf", "config", "bat", "rtf", "doc", "docx"];
|
||||
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
return allowedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
// Load the file list for a given folder.
|
||||
export function loadFileList(folderParam) {
|
||||
const folder = folderParam || "root";
|
||||
return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&t=" + new Date().getTime())
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
fileListContainer.innerHTML = "";
|
||||
if (data.files && data.files.length > 0) {
|
||||
fileData = data.files;
|
||||
renderFileTable(folder);
|
||||
} else {
|
||||
fileListContainer.textContent = "No files found.";
|
||||
updateFileActionButtons();
|
||||
}
|
||||
return data.files || [];
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error loading file list:", error);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Render the file table with pagination controls.
|
||||
export function renderFileTable(folder) {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
const folderPath = (folder === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + encodeURIComponent(folder) + "/";
|
||||
|
||||
// Pagination variables:
|
||||
const itemsPerPage = window.itemsPerPage || 10;
|
||||
const currentPage = window.currentPage || 1;
|
||||
const totalFiles = fileData.length;
|
||||
const totalPages = Math.ceil(totalFiles / itemsPerPage);
|
||||
|
||||
// Build table header.
|
||||
let tableHTML = `<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
|
||||
<th data-column="name" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="modified" class="hide-small" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="uploaded" class="hide-small" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="size" class="hide-small" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="uploader" class="hide-small" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>`;
|
||||
|
||||
// Calculate slice for current page.
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + itemsPerPage, totalFiles);
|
||||
let tableBody = `<tbody>`;
|
||||
|
||||
fileData.slice(startIndex, endIndex).forEach(file => {
|
||||
const isEditable = canEditFile(file.name);
|
||||
const safeFileName = escapeHTML(file.name);
|
||||
const safeModified = escapeHTML(file.modified);
|
||||
const safeUploaded = escapeHTML(file.uploaded);
|
||||
const safeSize = escapeHTML(file.size);
|
||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||
|
||||
tableBody += `<tr>
|
||||
<td><input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="updateFileActionButtons()"></td>
|
||||
<td>${safeFileName}</td>
|
||||
<td class="hide-small" style="white-space: nowrap;">${safeModified}</td>
|
||||
<td class="hide-small" style="white-space: nowrap;">${safeUploaded}</td>
|
||||
<td class="hide-small" style="white-space: nowrap;">${safeSize}</td>
|
||||
<td class="hide-small" style="white-space: nowrap;">${safeUploader}</td>
|
||||
<td>
|
||||
<div style="display: inline-flex; align-items: center; gap: 5px; flex-wrap: nowrap;">
|
||||
<a class="btn btn-sm btn-success" href="${folderPath + encodeURIComponent(file.name)}" download>Download</a>
|
||||
${isEditable ? `<button class="btn btn-sm btn-primary ml-2" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(folder)})'>Edit</button>` : ""}
|
||||
<button class="btn btn-sm btn-warning ml-2" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(folder)})'>Rename</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tableBody += `</tbody></table>`;
|
||||
|
||||
// Build pagination controls.
|
||||
let paginationHTML = `<div class="pagination-controls" style="margin-top:10px; display:flex; align-items:center; justify-content:space-between;">`;
|
||||
paginationHTML += `<div>`;
|
||||
paginationHTML += `<button ${currentPage === 1 ? "disabled" : ""} onclick="changePage(${currentPage - 1})">Prev</button> `;
|
||||
paginationHTML += `<span>Page ${currentPage} of ${totalPages}</span> `;
|
||||
paginationHTML += `<button ${currentPage === totalPages ? "disabled" : ""} onclick="changePage(${currentPage + 1})">Next</button>`;
|
||||
paginationHTML += `</div>`;
|
||||
paginationHTML += `<div>Show <select onchange="changeItemsPerPage(this.value)">`;
|
||||
[10, 20, 50, 100].forEach(num => {
|
||||
paginationHTML += `<option value="${num}" ${num === itemsPerPage ? "selected" : ""}>${num}</option>`;
|
||||
});
|
||||
paginationHTML += `</select> items per page</div>`;
|
||||
paginationHTML += `</div>`;
|
||||
|
||||
fileListContainer.innerHTML = tableHTML + tableBody + paginationHTML;
|
||||
|
||||
// Attach sorting event listeners on header cells.
|
||||
const headerCells = document.querySelectorAll("table.table thead th[data-column]");
|
||||
headerCells.forEach(cell => {
|
||||
cell.addEventListener("click", function () {
|
||||
const column = this.getAttribute("data-column");
|
||||
sortFiles(column, folder);
|
||||
});
|
||||
});
|
||||
|
||||
// After rendering the table, reattach the file checkbox event listener.
|
||||
document.querySelectorAll('#fileList .file-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFileActionButtons);
|
||||
});
|
||||
|
||||
// Finally, call updateFileActionButtons so the buttons show (or are disabled) correctly.
|
||||
updateFileActionButtons();
|
||||
}
|
||||
|
||||
// Sort files and re-render the table.
|
||||
export function sortFiles(column, folder) {
|
||||
if (sortOrder.column === column) {
|
||||
sortOrder.ascending = !sortOrder.ascending;
|
||||
} else {
|
||||
sortOrder.column = column;
|
||||
sortOrder.ascending = true;
|
||||
}
|
||||
|
||||
fileData.sort((a, b) => {
|
||||
let valA = a[column] || "";
|
||||
let valB = b[column] || "";
|
||||
|
||||
if (column === "modified" || column === "uploaded") {
|
||||
// Log the raw date strings.
|
||||
console.log(`Sorting ${column}: raw values ->`, valA, valB);
|
||||
|
||||
const parsedA = parseCustomDate(valA);
|
||||
const parsedB = parseCustomDate(valB);
|
||||
|
||||
// Log the parsed numeric timestamps.
|
||||
console.log(`Sorting ${column}: parsed values ->`, parsedA, parsedB);
|
||||
|
||||
valA = parsedA;
|
||||
valB = parsedB;
|
||||
} else if (typeof valA === "string") {
|
||||
valA = valA.toLowerCase();
|
||||
valB = valB.toLowerCase();
|
||||
}
|
||||
|
||||
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
||||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
renderFileTable(folder);
|
||||
}
|
||||
|
||||
// Delete selected files.
|
||||
export function handleDeleteSelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete the selected files?")) {
|
||||
return;
|
||||
}
|
||||
const filesToDelete = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("deleteFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: window.currentFolder, files: filesToDelete })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files deleted successfully!");
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not delete files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting files:", error));
|
||||
}
|
||||
|
||||
// Copy selected files.
|
||||
export function handleCopySelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected for copying.");
|
||||
return;
|
||||
}
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder for copying.");
|
||||
return;
|
||||
}
|
||||
const filesToCopy = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("copyFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ source: window.currentFolder, files: filesToCopy, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files copied successfully!");
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not copy files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error copying files:", error));
|
||||
}
|
||||
|
||||
// Move selected files.
|
||||
export function handleMoveSelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected for moving.");
|
||||
return;
|
||||
}
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder for moving.");
|
||||
return;
|
||||
}
|
||||
if (targetFolder === window.currentFolder) {
|
||||
alert("Error: Cannot move files to the same folder.");
|
||||
return;
|
||||
}
|
||||
const filesToMove = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("moveFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ source: window.currentFolder, files: filesToMove, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files moved successfully!");
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not move files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error moving files:", error));
|
||||
}
|
||||
|
||||
// File Editing Functions.
|
||||
export function editFile(fileName, folder) {
|
||||
console.log("Edit button clicked for:", fileName);
|
||||
let existingEditor = document.getElementById("editorContainer");
|
||||
if (existingEditor) { existingEditor.remove(); }
|
||||
const folderUsed = folder || window.currentFolder || "root";
|
||||
const folderPath = (folderUsed === "root")
|
||||
? "uploads/"
|
||||
: "uploads/" + encodeURIComponent(folderUsed) + "/";
|
||||
const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime();
|
||||
|
||||
fetch(fileUrl, { method: "HEAD" })
|
||||
.then(response => {
|
||||
const contentLength = response.headers.get("Content-Length");
|
||||
if (contentLength && parseInt(contentLength) > 10485760) {
|
||||
alert("This file is larger than 10 MB and cannot be edited in the browser.");
|
||||
throw new Error("File too large.");
|
||||
}
|
||||
return fetch(fileUrl);
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error! Status: " + response.status);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(content => {
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "editorContainer";
|
||||
modal.classList.add("modal", "editor-modal");
|
||||
modal.innerHTML = `
|
||||
<h3>Editing: ${fileName}</h3>
|
||||
<textarea id="fileEditor" style="width:100%; height:80%; resize:none;">${content}</textarea>
|
||||
<div style="margin-top:10px; text-align:right;">
|
||||
<button onclick="saveFile('${fileName}', '${folderUsed}')" class="btn btn-primary">Save</button>
|
||||
<button onclick="document.getElementById('editorContainer').remove()" class="btn btn-secondary">Close</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
})
|
||||
.catch(error => console.error("Error loading file:", error));
|
||||
}
|
||||
|
||||
export function saveFile(fileName, folder) {
|
||||
const editor = document.getElementById("fileEditor");
|
||||
if (!editor) {
|
||||
console.error("Editor not found!");
|
||||
return;
|
||||
}
|
||||
const folderUsed = folder || window.currentFolder || "root";
|
||||
const fileDataObj = {
|
||||
fileName: fileName,
|
||||
content: editor.value,
|
||||
folder: folderUsed
|
||||
};
|
||||
fetch("saveFile.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(fileDataObj)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
alert(result.success || result.error);
|
||||
document.getElementById("editorContainer")?.remove();
|
||||
loadFileList(folderUsed);
|
||||
})
|
||||
.catch(error => console.error("Error saving file:", error));
|
||||
}
|
||||
|
||||
// File Upload Handling: Display preview for image or file icon.
|
||||
export function displayFilePreview(file, container) {
|
||||
container.style.display = "inline-block";
|
||||
if (file.type.startsWith("image/")) {
|
||||
const img = document.createElement("img");
|
||||
img.src = URL.createObjectURL(file);
|
||||
img.style.maxWidth = "100px";
|
||||
img.style.maxHeight = "100px";
|
||||
img.style.marginRight = "5px";
|
||||
img.style.marginLeft = "0px";
|
||||
container.appendChild(img);
|
||||
} else {
|
||||
const iconSpan = document.createElement("span");
|
||||
iconSpan.classList.add("material-icons");
|
||||
iconSpan.style.color = "#333";
|
||||
iconSpan.textContent = "insert_drive_file";
|
||||
iconSpan.style.marginRight = "0px";
|
||||
iconSpan.style.marginLeft = "0px";
|
||||
iconSpan.style.fontSize = "32px";
|
||||
container.appendChild(iconSpan);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize file action buttons.
|
||||
export function initFileActions() {
|
||||
const deleteSelectedBtn = document.getElementById("deleteSelectedBtn");
|
||||
if (deleteSelectedBtn) {
|
||||
deleteSelectedBtn.replaceWith(deleteSelectedBtn.cloneNode(true));
|
||||
document.getElementById("deleteSelectedBtn").addEventListener("click", handleDeleteSelected);
|
||||
}
|
||||
|
||||
const copySelectedBtn = document.getElementById("copySelectedBtn");
|
||||
if (copySelectedBtn) {
|
||||
copySelectedBtn.replaceWith(copySelectedBtn.cloneNode(true));
|
||||
document.getElementById("copySelectedBtn").addEventListener("click", handleCopySelected);
|
||||
}
|
||||
|
||||
const moveSelectedBtn = document.getElementById("moveSelectedBtn");
|
||||
if (moveSelectedBtn) {
|
||||
moveSelectedBtn.replaceWith(moveSelectedBtn.cloneNode(true));
|
||||
document.getElementById("moveSelectedBtn").addEventListener("click", handleMoveSelected);
|
||||
}
|
||||
// No need to set display styles here; let updateFileActionButtons handle it.
|
||||
}
|
||||
|
||||
|
||||
// Rename function: always available.
|
||||
export function renameFile(oldName, folder) {
|
||||
const newName = prompt(`Enter new name for file "${oldName}":`, oldName);
|
||||
if (!newName || newName === oldName) {
|
||||
return; // No change.
|
||||
}
|
||||
const folderUsed = folder || window.currentFolder || "root";
|
||||
fetch("renameFile.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: folderUsed, oldName: oldName, newName: newName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("File renamed successfully!");
|
||||
loadFileList(folderUsed);
|
||||
} else {
|
||||
alert("Error renaming file: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error renaming file:", error);
|
||||
alert("Error renaming file");
|
||||
});
|
||||
}
|
||||
|
||||
// Expose renameFile to global scope.
|
||||
window.renameFile = renameFile;
|
||||
|
||||
// Global pagination functions.
|
||||
window.changePage = function (newPage) {
|
||||
window.currentPage = newPage;
|
||||
renderFileTable(window.currentFolder);
|
||||
};
|
||||
|
||||
window.changeItemsPerPage = function (newCount) {
|
||||
window.itemsPerPage = parseInt(newCount);
|
||||
window.currentPage = 1; // Reset to first page.
|
||||
renderFileTable(window.currentFolder);
|
||||
};
|
||||
215
folderManager.js
Normal file
215
folderManager.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// folderManager.js
|
||||
import {
|
||||
loadFileList
|
||||
} from './fileManager.js';
|
||||
|
||||
|
||||
export function renameFolder() {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
const newFolderName = prompt("Enter the new folder name:", selectedFolder);
|
||||
if (newFolderName && newFolderName !== selectedFolder) {
|
||||
fetch("renameFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ oldName: selectedFolder, newName: newFolderName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder renamed successfully!");
|
||||
loadFolderList("root");
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not rename folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error renaming folder:", error));
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteFolder() {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
alert("Please select a valid folder to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only prompt once.
|
||||
if (!confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Proceed with deletion.
|
||||
fetch("deleteFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: selectedFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder deleted successfully!");
|
||||
// Refresh both folder dropdowns.
|
||||
loadFolderList("root");
|
||||
loadCopyMoveFolderList();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not delete folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting folder:", error));
|
||||
}
|
||||
|
||||
|
||||
// Updates the copy/move folder dropdown.
|
||||
export async function loadCopyMoveFolderList() {
|
||||
try {
|
||||
const response = await fetch('getFolderList.php');
|
||||
const folders = await response.json();
|
||||
const folderSelect = document.getElementById('copyMoveFolderSelect');
|
||||
folderSelect.innerHTML = ''; // Clear existing options
|
||||
|
||||
// Always add a "Root" option as the default.
|
||||
const rootOption = document.createElement('option');
|
||||
rootOption.value = 'root';
|
||||
rootOption.textContent = '(Root)';
|
||||
folderSelect.appendChild(rootOption);
|
||||
|
||||
if (Array.isArray(folders) && folders.length > 0) {
|
||||
folders.forEach(folder => {
|
||||
const option = document.createElement('option');
|
||||
option.value = folder;
|
||||
option.textContent = folder;
|
||||
folderSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading folder list:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Optional helper to load folder lists (alias for loadCopyMoveFolderList).
|
||||
|
||||
export function loadFolderList(selectedFolder) {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
folderSelect.innerHTML = "";
|
||||
const rootOption = document.createElement("option");
|
||||
rootOption.value = "root";
|
||||
rootOption.textContent = "(Root)";
|
||||
folderSelect.appendChild(rootOption);
|
||||
|
||||
fetch("getFolderList.php")
|
||||
.then(response => response.json())
|
||||
.then(folders => {
|
||||
folders.forEach(function (folder) {
|
||||
let option = document.createElement("option");
|
||||
option.value = folder;
|
||||
option.textContent = folder;
|
||||
folderSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Set the selected folder if provided, else default to "root"
|
||||
if (selectedFolder && [...folderSelect.options].some(opt => opt.value === selectedFolder)) {
|
||||
folderSelect.value = selectedFolder;
|
||||
} else {
|
||||
folderSelect.value = "root";
|
||||
}
|
||||
|
||||
// Update global currentFolder and title, then load the file list
|
||||
window.currentFolder = folderSelect.value;
|
||||
document.getElementById("fileListTitle").textContent =
|
||||
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
|
||||
loadFileList(window.currentFolder);
|
||||
})
|
||||
.catch(error => console.error("Error loading folder list:", error));
|
||||
}
|
||||
|
||||
// Event listener for folder dropdown changes
|
||||
document.getElementById("folderSelect").addEventListener("change", function () {
|
||||
window.currentFolder = this.value;
|
||||
document.getElementById("fileListTitle").textContent =
|
||||
window.currentFolder === "root" ? "Files in (Root)" : "Files in (" + window.currentFolder + ")";
|
||||
loadFileList(window.currentFolder);
|
||||
});
|
||||
|
||||
// Event listener for creating a folder
|
||||
document.getElementById("createFolderBtn").addEventListener("click", function () {
|
||||
let folderName = prompt("Enter folder name:");
|
||||
if (folderName) {
|
||||
fetch("createFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: folderName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder created successfully!");
|
||||
loadFolderList(folderName);
|
||||
loadCopyMoveFolderList();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not create folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error creating folder:", error));
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for renaming a folder
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", function () {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
alert("Please select a valid folder to rename.");
|
||||
return;
|
||||
}
|
||||
let newFolderName = prompt("Enter new folder name for '" + selectedFolder + "':", selectedFolder);
|
||||
if (newFolderName && newFolderName !== selectedFolder) {
|
||||
fetch("renameFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder renamed successfully!");
|
||||
loadCopyMoveFolderList()
|
||||
loadFolderList(newFolderName);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not rename folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error renaming folder:", error));
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for deleting a folder
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", function () {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
alert("Please select a valid folder to delete.");
|
||||
return;
|
||||
}
|
||||
if (confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
|
||||
fetch("deleteFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: selectedFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder deleted successfully!");
|
||||
loadFolderList("root");
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not delete folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting folder:", error));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
session_start();
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
|
||||
@@ -127,9 +127,7 @@
|
||||
|
||||
<!-- JavaScript Files -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||
<script type="module" src="utils.js"></script>
|
||||
<script type="module" src="auth.js"></script>
|
||||
<script type="module" src="upload.js"></script>
|
||||
<script type="module" src="displayFileList.js"></script>
|
||||
<script type="module" src="main.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
48
main.js
Normal file
48
main.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// main.js
|
||||
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import {
|
||||
toggleVisibility,
|
||||
toggleAllCheckboxes,
|
||||
updateFileActionButtons
|
||||
} from './domUtils.js';
|
||||
import {
|
||||
loadFileList,
|
||||
initFileActions,
|
||||
editFile,
|
||||
saveFile,
|
||||
displayFilePreview,
|
||||
renameFile
|
||||
} from './fileManager.js';
|
||||
import {
|
||||
deleteFolder,
|
||||
loadCopyMoveFolderList,
|
||||
loadFolderList
|
||||
} from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth } from './auth.js';
|
||||
|
||||
// Expose functions for inline handlers.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
window.toggleAllCheckboxes = toggleAllCheckboxes;
|
||||
window.editFile = editFile;
|
||||
window.saveFile = saveFile;
|
||||
window.renameFile = renameFile;
|
||||
|
||||
|
||||
// Global variable for the current folder.
|
||||
window.currentFolder = "root";
|
||||
|
||||
// DOMContentLoaded initialization.
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
window.currentFolder = window.currentFolder || "root";
|
||||
loadFileList(window.currentFolder);
|
||||
loadCopyMoveFolderList();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderList();
|
||||
updateFileActionButtons();
|
||||
// Initialize authentication and user management.
|
||||
initAuth();
|
||||
});
|
||||
22
networkUtils.js
Normal file
22
networkUtils.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// networkUtils.js
|
||||
export function sendRequest(url, method = "GET", data = null) {
|
||||
console.log("Sending request to:", url, "with method:", method);
|
||||
const options = { method, headers: { "Content-Type": "application/json" } };
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
return fetch(url, options)
|
||||
.then(response => {
|
||||
console.log("Response status:", response.status);
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json().catch(() => {
|
||||
console.warn("Response is not JSON, returning as text");
|
||||
return response.text();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
62
renameFile.php
Normal file
62
renameFile.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
session_start();
|
||||
header('Content-Type: application/json');
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data || !isset($data['folder']) || !isset($data['oldName']) || !isset($data['newName'])) {
|
||||
echo json_encode(["error" => "Invalid input"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = trim($data['folder']) ?: 'root';
|
||||
$oldName = basename(trim($data['oldName']));
|
||||
$newName = basename(trim($data['newName']));
|
||||
|
||||
if ($folder !== 'root') {
|
||||
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$directory = UPLOAD_DIR;
|
||||
}
|
||||
|
||||
$oldPath = $directory . $oldName;
|
||||
$newPath = $directory . $newName;
|
||||
|
||||
if (!file_exists($oldPath)) {
|
||||
echo json_encode(["error" => "File does not exist"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (file_exists($newPath)) {
|
||||
echo json_encode(["error" => "A file with the new name already exists"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$metadataFile = META_DIR . META_FILE;
|
||||
|
||||
if (rename($oldPath, $newPath)) {
|
||||
// Update metadata.
|
||||
if (file_exists($metadataFile)) {
|
||||
$metadata = json_decode(file_get_contents($metadataFile), true);
|
||||
// Build the keys.
|
||||
$oldKey = ($folder !== 'root') ? $folder . "/" . $oldName : $oldName;
|
||||
$newKey = ($folder !== 'root') ? $folder . "/" . $newName : $newName;
|
||||
if (isset($metadata[$oldKey])) {
|
||||
$metadata[$newKey] = $metadata[$oldKey];
|
||||
unset($metadata[$oldKey]);
|
||||
file_put_contents($metadataFile, json_encode($metadata, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
echo json_encode(["success" => "File renamed successfully"]);
|
||||
} else {
|
||||
echo json_encode(["error" => "Error renaming file"]);
|
||||
}
|
||||
?>
|
||||
61
styles.css
61
styles.css
@@ -33,7 +33,12 @@ header {
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1 1 auto;
|
||||
position: absolute;
|
||||
/* Absolutely position the title */
|
||||
left: 50%;
|
||||
/* Position it 50% from the left edge */
|
||||
transform: translateX(-50%);
|
||||
/* Center it by moving it left by 50% of its own width */
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
color: white;
|
||||
@@ -64,6 +69,7 @@ header {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
@@ -86,7 +92,8 @@ header {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(75%, 75%); /* centers the modal */
|
||||
transform: translate(75%, 75%);
|
||||
/* centers the modal */
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
@@ -98,11 +105,13 @@ header {
|
||||
height: 600px;
|
||||
max-height: 35vh;
|
||||
}
|
||||
|
||||
.editor-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(5%, 10%); /* centers the editor modal */
|
||||
transform: translate(5%, 10%);
|
||||
/* centers the editor modal */
|
||||
width: 50vw;
|
||||
max-width: 90vw;
|
||||
min-width: 400px;
|
||||
@@ -129,28 +138,34 @@ header {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#uploadProgressContainer li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#uploadProgressContainer .file-preview {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
margin-right: 1px;
|
||||
margin-right: 0px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#uploadProgressContainer .file-preview img {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#uploadProgressContainer .file-name {
|
||||
margin-right: 20px;
|
||||
margin-left: 2px;
|
||||
flex-grow: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#uploadProgressContainer .progress {
|
||||
background-color: #e9ecef;
|
||||
border-radius: 5px;
|
||||
@@ -160,6 +175,7 @@ header {
|
||||
height: 24px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#uploadProgressContainer .progress-bar {
|
||||
background-color: #007bff;
|
||||
height: 100%;
|
||||
@@ -169,6 +185,7 @@ header {
|
||||
transition: width 0.4s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#uploadProgressContainer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -181,10 +198,16 @@ header {
|
||||
text-align: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.logout-container button {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.hide-small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* BUTTON STYLES (MATERIAL THEME) */
|
||||
@@ -198,9 +221,11 @@ header {
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* File list action buttons (for Delete, Copy, Move) */
|
||||
.file-list-actions button {
|
||||
background-color: #2196F3;
|
||||
@@ -211,12 +236,14 @@ header {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-list-actions button:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
|
||||
#deleteSelectedBtn {
|
||||
background-color: #f44336; /* Material red */
|
||||
background-color: #f44336;
|
||||
/* Material red */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -228,7 +255,8 @@ header {
|
||||
}
|
||||
|
||||
#copySelectedBtn {
|
||||
background-color: #9E9E9E; /* Material grey */
|
||||
background-color: #9E9E9E;
|
||||
/* Material grey */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -240,7 +268,8 @@ header {
|
||||
}
|
||||
|
||||
#moveSelectedBtn {
|
||||
background-color: #ff9800; /* Material orange */
|
||||
background-color: #ff9800;
|
||||
/* Material orange */
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
@@ -261,6 +290,7 @@ header {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#fileList button.edit-btn:hover {
|
||||
background-color: #43A047;
|
||||
}
|
||||
@@ -273,6 +303,7 @@ header {
|
||||
align-items: center;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.folder-dropdown {
|
||||
width: 100px;
|
||||
}
|
||||
@@ -282,15 +313,20 @@ header {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#fileList table th,
|
||||
#fileList table td {
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
border: none; /* Remove table borders */
|
||||
border: none;
|
||||
/* Remove table borders */
|
||||
}
|
||||
|
||||
#fileList table tr:nth-child(even) {
|
||||
background-color: transparent; /* Remove alternating grey rows */
|
||||
background-color: transparent;
|
||||
/* Remove alternating grey rows */
|
||||
}
|
||||
|
||||
#fileList table tr:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
@@ -299,6 +335,7 @@ header {
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -312,6 +349,7 @@ label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table th button {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -321,7 +359,8 @@ label {
|
||||
}
|
||||
|
||||
/* INITIAL HIDE FORMS */
|
||||
#loginForm, #uploadForm {
|
||||
#loginForm,
|
||||
#uploadForm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -337,4 +376,4 @@ label {
|
||||
|
||||
#fileListContainer {
|
||||
margin-top: 40px !important;
|
||||
}
|
||||
}
|
||||
310
upload.js
310
upload.js
@@ -1,129 +1,209 @@
|
||||
// upload.js
|
||||
import { displayFilePreview } from './utils.js';
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
import { loadFileList, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
|
||||
export function initUpload() {
|
||||
const fileInput = document.getElementById("file");
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
const uploadForm = document.getElementById("uploadFileForm");
|
||||
|
||||
function updateUploadProgress(e, listItem) {
|
||||
if (e.lengthComputable) {
|
||||
const currentPercent = Math.round((e.loaded / e.total) * 100);
|
||||
const elapsedTime = (Date.now() - listItem.startTime) / 1000;
|
||||
let speedText = "";
|
||||
if (elapsedTime > 0) {
|
||||
const speed = e.loaded / elapsedTime;
|
||||
if (speed < 1024) {
|
||||
speedText = speed.toFixed(0) + " B/s";
|
||||
} else if (speed < 1048576) {
|
||||
speedText = (speed / 1024).toFixed(1) + " KB/s";
|
||||
} else {
|
||||
speedText = (speed / 1048576).toFixed(1) + " MB/s";
|
||||
// Build progress list when files are selected.
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", function () {
|
||||
progressContainer.innerHTML = "";
|
||||
const files = fileInput.files;
|
||||
if (files.length > 0) {
|
||||
const allFiles = Array.from(files);
|
||||
const maxDisplay = 10;
|
||||
const list = document.createElement("ul");
|
||||
list.style.listStyle = "none";
|
||||
list.style.padding = "0";
|
||||
allFiles.forEach((file, index) => {
|
||||
const li = document.createElement("li");
|
||||
li.style.paddingTop = "20px";
|
||||
li.style.marginBottom = "10px";
|
||||
// Only display progress items for the first maxDisplay files.
|
||||
li.style.display = (index < maxDisplay) ? "flex" : "none";
|
||||
li.style.alignItems = "center";
|
||||
li.style.flexWrap = "wrap";
|
||||
|
||||
const preview = document.createElement("div");
|
||||
preview.className = "file-preview";
|
||||
displayFilePreview(file, preview);
|
||||
|
||||
const nameDiv = document.createElement("div");
|
||||
nameDiv.textContent = file.name;
|
||||
nameDiv.style.flexGrow = "1";
|
||||
nameDiv.style.marginLeft = "5px";
|
||||
nameDiv.style.wordBreak = "break-word";
|
||||
|
||||
const progDiv = document.createElement("div");
|
||||
progDiv.classList.add("progress");
|
||||
progDiv.style.flex = "0 0 250px";
|
||||
progDiv.style.marginLeft = "5px";
|
||||
|
||||
const progBar = document.createElement("div");
|
||||
progBar.classList.add("progress-bar");
|
||||
progBar.style.width = "0%";
|
||||
progBar.innerText = "0%";
|
||||
|
||||
progDiv.appendChild(progBar);
|
||||
li.appendChild(preview);
|
||||
li.appendChild(nameDiv);
|
||||
li.appendChild(progDiv);
|
||||
// Save references for later updates.
|
||||
li.progressBar = progBar;
|
||||
li.startTime = Date.now();
|
||||
list.appendChild(li);
|
||||
});
|
||||
// If more than maxDisplay files, add a note.
|
||||
if (allFiles.length > maxDisplay) {
|
||||
const extra = document.createElement("li");
|
||||
extra.style.paddingTop = "20px";
|
||||
extra.style.marginBottom = "10px";
|
||||
extra.textContent = `Uploading additional ${allFiles.length - maxDisplay} file(s)...`;
|
||||
extra.style.display = "flex";
|
||||
list.appendChild(extra);
|
||||
}
|
||||
progressContainer.appendChild(list);
|
||||
}
|
||||
listItem.progressBar.style.width = currentPercent + "%";
|
||||
listItem.progressBar.innerText = currentPercent + "% (" + speedText + ")";
|
||||
return currentPercent;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", function () {
|
||||
progressContainer.innerHTML = "";
|
||||
const files = fileInput.files;
|
||||
if (files.length > 0) {
|
||||
const list = document.createElement("ul");
|
||||
list.style.listStyle = "none";
|
||||
list.style.padding = "0";
|
||||
Array.from(files).forEach((file) => {
|
||||
const listItem = document.createElement("li");
|
||||
listItem.style.paddingTop = "20px";
|
||||
listItem.style.marginBottom = "10px";
|
||||
listItem.style.display = "flex";
|
||||
listItem.style.alignItems = "center";
|
||||
listItem.style.flexWrap = "wrap";
|
||||
|
||||
const previewContainer = document.createElement("div");
|
||||
previewContainer.className = "file-preview";
|
||||
displayFilePreview(file, previewContainer);
|
||||
|
||||
const fileNameDiv = document.createElement("div");
|
||||
fileNameDiv.textContent = file.name;
|
||||
fileNameDiv.style.flexGrow = "1";
|
||||
fileNameDiv.style.marginLeft = "5px";
|
||||
fileNameDiv.style.wordBreak = "break-word";
|
||||
|
||||
const progressDiv = document.createElement("div");
|
||||
progressDiv.classList.add("progress");
|
||||
progressDiv.style.flex = "0 0 250px";
|
||||
progressDiv.style.marginLeft = "5px";
|
||||
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.classList.add("progress-bar");
|
||||
progressBar.style.width = "0%";
|
||||
progressBar.innerText = "0%";
|
||||
|
||||
progressDiv.appendChild(progressBar);
|
||||
|
||||
listItem.appendChild(previewContainer);
|
||||
listItem.appendChild(fileNameDiv);
|
||||
listItem.appendChild(progressDiv);
|
||||
|
||||
listItem.progressBar = progressBar;
|
||||
listItem.startTime = Date.now();
|
||||
|
||||
list.appendChild(listItem);
|
||||
});
|
||||
progressContainer.appendChild(list);
|
||||
}
|
||||
});
|
||||
|
||||
uploadForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const files = fileInput.files;
|
||||
if (files.length === 0) {
|
||||
alert("No files selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
const listItems = progressContainer.querySelectorAll("li");
|
||||
let finishedCount = 0;
|
||||
|
||||
Array.from(files).forEach((file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file[]", file);
|
||||
const folderElem = document.getElementById("folderSelect");
|
||||
if (folderElem) {
|
||||
formData.append("folder", folderElem.value);
|
||||
} else {
|
||||
console.error("Folder selection element not found!");
|
||||
// Submit handler: upload all files and then check the server's file list.
|
||||
if (uploadForm) {
|
||||
uploadForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const files = fileInput.files;
|
||||
if (files.length === 0) {
|
||||
alert("No files selected.");
|
||||
return;
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
let currentPercent = 0;
|
||||
xhr.upload.addEventListener("progress", function (e) {
|
||||
currentPercent = updateUploadProgress(e, listItems[index]);
|
||||
});
|
||||
xhr.addEventListener("load", function () {
|
||||
if (currentPercent >= 100) {
|
||||
listItems[index].progressBar.innerText = "Done";
|
||||
}
|
||||
finishedCount++;
|
||||
console.log("Upload response for file", file.name, xhr.responseText);
|
||||
if (finishedCount === files.length) {
|
||||
if (typeof loadFileList === "function") {
|
||||
loadFileList();
|
||||
const allFiles = Array.from(files);
|
||||
const maxDisplay = 10; // Only show progress for first 10 items.
|
||||
const folderToUse = window.currentFolder || "root";
|
||||
const listItems = progressContainer.querySelectorAll("li");
|
||||
let finishedCount = 0;
|
||||
let allSucceeded = true;
|
||||
// Array to track each file's upload result.
|
||||
const uploadResults = new Array(allFiles.length).fill(false);
|
||||
|
||||
allFiles.forEach((file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file[]", file);
|
||||
formData.append("folder", folderToUse);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
let currentPercent = 0;
|
||||
|
||||
xhr.upload.addEventListener("progress", function (e) {
|
||||
if (e.lengthComputable) {
|
||||
currentPercent = Math.round((e.loaded / e.total) * 100);
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
const elapsed = (Date.now() - listItems[index].startTime) / 1000;
|
||||
let speed = "";
|
||||
if (elapsed > 0) {
|
||||
const spd = e.loaded / elapsed;
|
||||
if (spd < 1024) speed = spd.toFixed(0) + " B/s";
|
||||
else if (spd < 1048576) speed = (spd / 1024).toFixed(1) + " KB/s";
|
||||
else speed = (spd / 1048576).toFixed(1) + " MB/s";
|
||||
}
|
||||
listItems[index].progressBar.style.width = currentPercent + "%";
|
||||
listItems[index].progressBar.innerText = currentPercent + "% (" + speed + ")";
|
||||
}
|
||||
}
|
||||
fileInput.value = "";
|
||||
setTimeout(() => {
|
||||
progressContainer.innerHTML = "";
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", function () {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
jsonResponse = null;
|
||||
}
|
||||
if (xhr.status >= 200 && xhr.status < 300 && (!jsonResponse || !jsonResponse.error)) {
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
listItems[index].progressBar.style.width = "100%";
|
||||
listItems[index].progressBar.innerText = "Done";
|
||||
}
|
||||
uploadResults[index] = true;
|
||||
} else {
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
listItems[index].progressBar.innerText = "Error";
|
||||
}
|
||||
allSucceeded = false;
|
||||
}
|
||||
finishedCount++;
|
||||
console.log("Upload response for file", file.name, xhr.responseText);
|
||||
if (finishedCount === allFiles.length) {
|
||||
// Immediately refresh the file list.
|
||||
refreshFileList();
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", function () {
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
listItems[index].progressBar.innerText = "Error";
|
||||
}
|
||||
uploadResults[index] = false;
|
||||
allSucceeded = false;
|
||||
finishedCount++;
|
||||
console.error("Error uploading file:", file.name);
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList();
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("abort", function () {
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
listItems[index].progressBar.innerText = "Aborted";
|
||||
}
|
||||
uploadResults[index] = false;
|
||||
allSucceeded = false;
|
||||
finishedCount++;
|
||||
console.error("Upload aborted for file:", file.name);
|
||||
if (finishedCount === allFiles.length) {
|
||||
refreshFileList();
|
||||
}
|
||||
});
|
||||
|
||||
xhr.open("POST", "upload.php", true);
|
||||
xhr.send(formData);
|
||||
});
|
||||
xhr.addEventListener("error", function () {
|
||||
listItems[index].progressBar.innerText = "Error";
|
||||
});
|
||||
xhr.open("POST", "upload.php", true);
|
||||
xhr.send(formData);
|
||||
|
||||
function refreshFileList() {
|
||||
// Call loadFileList immediately (with a timestamp added inside loadFileList, if needed).
|
||||
loadFileList(folderToUse)
|
||||
.then(serverFiles => {
|
||||
initFileActions();
|
||||
// Normalize server file names to lowercase.
|
||||
serverFiles = (serverFiles || []).map(item => item.name.trim().toLowerCase());
|
||||
// For each file, if it was successful and is present on the server, leave its progress item;
|
||||
// if not, mark it as "Error".
|
||||
allFiles.forEach((file, index) => {
|
||||
const fileName = file.name.trim().toLowerCase();
|
||||
if (index < maxDisplay && listItems[index]) {
|
||||
if (!uploadResults[index] || !serverFiles.includes(fileName)) {
|
||||
listItems[index].progressBar.innerText = "Error";
|
||||
allSucceeded = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
// Now, the file list is refreshed instantly.
|
||||
// However, we want the progress list to remain visible for 10 seconds.
|
||||
setTimeout(() => {
|
||||
progressContainer.innerHTML = "";
|
||||
fileInput.value = "";
|
||||
}, 10000);
|
||||
if (!allSucceeded) {
|
||||
alert("Some files failed to upload. Please check the list.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error fetching file list:", error);
|
||||
alert("Some files may have failed to upload. Please check the list.");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
880
utils.js
880
utils.js
@@ -1,880 +0,0 @@
|
||||
// =======================
|
||||
// Utility Functions
|
||||
// =======================
|
||||
let fileData = []; // will store the fetched file data
|
||||
let sortOrder = { column: "uploaded", ascending: true };
|
||||
|
||||
/**
|
||||
* Sends an AJAX request using the Fetch API.
|
||||
* @param {string} url - The endpoint URL.
|
||||
* @param {string} [method="GET"] - The HTTP method.
|
||||
* @param {object|null} [data=null] - The payload to send (for POST/PUT).
|
||||
* @returns {Promise} Resolves with JSON (or text) response or rejects with an error.
|
||||
*/
|
||||
export function sendRequest(url, method = "GET", data = null) {
|
||||
console.log("Sending request to:", url, "with method:", method);
|
||||
const options = { method, headers: { "Content-Type": "application/json" } };
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
return fetch(url, options)
|
||||
.then(response => {
|
||||
console.log("Response status:", response.status);
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json().catch(() => {
|
||||
console.warn("Response is not JSON, returning as text");
|
||||
return response.text();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the display of an element by its ID.
|
||||
* @param {string} elementId - The element’s ID.
|
||||
* @param {boolean} shouldShow - True to display the element, false to hide.
|
||||
*/
|
||||
export function toggleVisibility(elementId, shouldShow) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.style.display = shouldShow ? "block" : "none";
|
||||
} else {
|
||||
console.error(`Element with id "${elementId}" not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Expose utilities to the global scope.
|
||||
window.sendRequest = sendRequest;
|
||||
window.toggleVisibility = toggleVisibility;
|
||||
|
||||
// =======================
|
||||
// Application Code
|
||||
// =======================
|
||||
|
||||
// Global variables
|
||||
let currentFolder = "root";
|
||||
let setupMode = false;
|
||||
|
||||
/**
|
||||
* Determines if a file is editable based on its extension.
|
||||
* @param {string} fileName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
function canEditFile(fileName) {
|
||||
const allowedExtensions = ["txt", "html", "htm", "php", "css", "js", "json", "xml", "md", "py", "ini", "csv", "log", "conf", "config", "bat", "rtf", "doc", "docx"];
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length < 2) return false;
|
||||
const ext = parts.pop().toLowerCase();
|
||||
return allowedExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a file preview (either an image or an icon) in a container.
|
||||
* @param {File} file - The file to preview.
|
||||
* @param {HTMLElement} container - The container to append the preview.
|
||||
*/
|
||||
export function displayFilePreview(file, container) {
|
||||
if (file.type.startsWith("image/")) {
|
||||
const img = document.createElement("img");
|
||||
img.style.width = "32px";
|
||||
img.style.height = "32px";
|
||||
img.style.objectFit = "cover";
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
container.appendChild(img);
|
||||
} else {
|
||||
const icon = document.createElement("i");
|
||||
icon.className = "material-icons";
|
||||
icon.style.fontSize = "32px";
|
||||
icon.style.color = "#555";
|
||||
icon.textContent = "insert_drive_file";
|
||||
container.appendChild(icon);
|
||||
}
|
||||
}
|
||||
|
||||
// =======================
|
||||
// DOMContentLoaded
|
||||
// =======================
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
checkAuthentication();
|
||||
|
||||
/**
|
||||
* Updates the UI based on authentication and setup data.
|
||||
* @param {object} data
|
||||
*/
|
||||
function updateUI(data) {
|
||||
console.log("Auth data:", data);
|
||||
if (data.setup) {
|
||||
setupMode = true;
|
||||
toggleVisibility("loginForm", false);
|
||||
document.getElementById("mainOperations").style.display = "none";
|
||||
document.getElementById("fileListContainer").style.display = "none";
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
document.getElementById("addUserModal").style.display = "block";
|
||||
return;
|
||||
} else {
|
||||
setupMode = false;
|
||||
}
|
||||
if (data.authenticated) {
|
||||
toggleVisibility("loginForm", false);
|
||||
document.getElementById("mainOperations").style.display = "block";
|
||||
document.getElementById("fileListContainer").style.display = "block";
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
if (data.isAdmin) {
|
||||
document.getElementById("logoutBtn").style.display = "block";
|
||||
document.getElementById("addUserBtn").style.display = "block";
|
||||
document.getElementById("removeUserBtn").style.display = "block";
|
||||
} else {
|
||||
document.getElementById("logoutBtn").style.display = "block";
|
||||
document.getElementById("addUserBtn").style.display = "none";
|
||||
document.getElementById("removeUserBtn").style.display = "none";
|
||||
}
|
||||
loadFolderList();
|
||||
} else {
|
||||
// Show login form if not authenticated.
|
||||
toggleVisibility("loginForm", true);
|
||||
document.getElementById("mainOperations").style.display = "none";
|
||||
document.getElementById("fileListContainer").style.display = "none";
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is authenticated.
|
||||
*/
|
||||
function checkAuthentication() {
|
||||
sendRequest("checkAuth.php")
|
||||
.then(updateUI)
|
||||
.catch(error => console.error("Error checking authentication:", error));
|
||||
}
|
||||
window.checkAuthentication = checkAuthentication;
|
||||
|
||||
// -----------------------
|
||||
// Authentication Form
|
||||
// -----------------------
|
||||
document.getElementById("authForm").addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
const formData = {
|
||||
username: document.getElementById("loginUsername").value.trim(),
|
||||
password: document.getElementById("loginPassword").value.trim()
|
||||
};
|
||||
sendRequest("auth.php", "POST", formData)
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
updateUI({ authenticated: true, isAdmin: data.isAdmin });
|
||||
} else {
|
||||
alert("Login failed: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error logging in:", error));
|
||||
});
|
||||
|
||||
document.getElementById("logoutBtn").addEventListener("click", function () {
|
||||
fetch("logout.php", { method: "POST" })
|
||||
.then(() => window.location.reload(true))
|
||||
.catch(error => console.error("Logout error:", error));
|
||||
});
|
||||
|
||||
// -----------------------
|
||||
// Add User Functionality
|
||||
// -----------------------
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
document.getElementById("addUserModal").style.display = "block";
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
if (!newUsername || !newPassword) {
|
||||
alert("Username and password are required!");
|
||||
return;
|
||||
}
|
||||
let url = "addUser.php";
|
||||
if (setupMode) {
|
||||
url += "?setup=1";
|
||||
}
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: newUsername, password: newPassword, isAdmin })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("User added successfully!");
|
||||
closeAddUserModal();
|
||||
checkAuthentication();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not add user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error adding user:", error));
|
||||
});
|
||||
document.getElementById("cancelUserBtn").addEventListener("click", function () {
|
||||
closeAddUserModal();
|
||||
});
|
||||
|
||||
// -----------------------
|
||||
// Remove User Functionality
|
||||
// -----------------------
|
||||
document.getElementById("removeUserBtn").addEventListener("click", function () {
|
||||
loadUserList();
|
||||
document.getElementById("removeUserModal").style.display = "block";
|
||||
});
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
if (!usernameToRemove) {
|
||||
alert("Please select a user to remove.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
|
||||
return;
|
||||
}
|
||||
fetch("removeUser.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username: usernameToRemove })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("User removed successfully!");
|
||||
closeRemoveUserModal();
|
||||
loadUserList();
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not remove user"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error removing user:", error));
|
||||
});
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
|
||||
closeRemoveUserModal();
|
||||
});
|
||||
|
||||
function closeAddUserModal() {
|
||||
document.getElementById("addUserModal").style.display = "none";
|
||||
resetUserForm();
|
||||
}
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
}
|
||||
function closeRemoveUserModal() {
|
||||
document.getElementById("removeUserModal").style.display = "none";
|
||||
document.getElementById("removeUsernameSelect").innerHTML = "";
|
||||
}
|
||||
function loadUserList() {
|
||||
fetch("getUsers.php")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const users = Array.isArray(data) ? data : (data.users || []);
|
||||
if (!users || !Array.isArray(users)) {
|
||||
console.error("Invalid users data:", data);
|
||||
return;
|
||||
}
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
selectElem.innerHTML = "";
|
||||
users.forEach(user => {
|
||||
const option = document.createElement("option");
|
||||
option.value = user.username;
|
||||
option.textContent = user.username;
|
||||
selectElem.appendChild(option);
|
||||
});
|
||||
if (selectElem.options.length === 0) {
|
||||
alert("No other users found to remove.");
|
||||
closeRemoveUserModal();
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading user list:", error));
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Folder Management
|
||||
// -----------------------
|
||||
function loadFolderList(selectedFolder) {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
folderSelect.innerHTML = "";
|
||||
const rootOption = document.createElement("option");
|
||||
rootOption.value = "root";
|
||||
rootOption.textContent = "(Root)";
|
||||
folderSelect.appendChild(rootOption);
|
||||
fetch("getFolderList.php")
|
||||
.then(response => response.json())
|
||||
.then(folders => {
|
||||
folders.forEach(function (folder) {
|
||||
let option = document.createElement("option");
|
||||
option.value = folder;
|
||||
option.textContent = folder;
|
||||
folderSelect.appendChild(option);
|
||||
});
|
||||
if (selectedFolder && [...folderSelect.options].some(opt => opt.value === selectedFolder)) {
|
||||
folderSelect.value = selectedFolder;
|
||||
} else {
|
||||
folderSelect.value = "root";
|
||||
}
|
||||
currentFolder = folderSelect.value;
|
||||
document.getElementById("fileListTitle").textContent =
|
||||
currentFolder === "root" ? "Files in (Root)" : "Files in (" + currentFolder + ")";
|
||||
loadFileList(currentFolder);
|
||||
})
|
||||
.catch(error => console.error("Error loading folder list:", error));
|
||||
}
|
||||
document.getElementById("folderSelect").addEventListener("change", function () {
|
||||
currentFolder = this.value;
|
||||
document.getElementById("fileListTitle").textContent =
|
||||
currentFolder === "root" ? "Files in (Root)" : "Files in (" + currentFolder + ")";
|
||||
loadFileList(currentFolder);
|
||||
});
|
||||
document.getElementById("createFolderBtn").addEventListener("click", function () {
|
||||
let folderName = prompt("Enter folder name:");
|
||||
if (folderName) {
|
||||
fetch("createFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: folderName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder created successfully!");
|
||||
loadFolderList(folderName);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not create folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error creating folder:", error));
|
||||
}
|
||||
});
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", function () {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
alert("Please select a valid folder to rename.");
|
||||
return;
|
||||
}
|
||||
let newFolderName = prompt("Enter new folder name for '" + selectedFolder + "':", selectedFolder);
|
||||
if (newFolderName && newFolderName !== selectedFolder) {
|
||||
fetch("renameFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ oldFolder: selectedFolder, newFolder: newFolderName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder renamed successfully!");
|
||||
loadFolderList(newFolderName);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not rename folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error renaming folder:", error));
|
||||
}
|
||||
});
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", function () {
|
||||
const folderSelect = document.getElementById("folderSelect");
|
||||
const selectedFolder = folderSelect.value;
|
||||
if (!selectedFolder || selectedFolder === "root") {
|
||||
alert("Please select a valid folder to delete.");
|
||||
return;
|
||||
}
|
||||
if (confirm("Are you sure you want to delete folder " + selectedFolder + "?")) {
|
||||
fetch("deleteFolder.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: selectedFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Folder deleted successfully!");
|
||||
loadFolderList("root");
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not delete folder"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting folder:", error));
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------
|
||||
// File List Management
|
||||
// -----------------------
|
||||
|
||||
// Load the file list for a given folder (defaults to currentFolder or "root")
|
||||
function loadFileList(folderParam) {
|
||||
const folder = folderParam || currentFolder || "root";
|
||||
fetch("getFileList.php?folder=" + encodeURIComponent(folder))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
fileListContainer.innerHTML = "";
|
||||
if (data.files && data.files.length > 0) {
|
||||
// Save the file list globally for sorting
|
||||
fileData = data.files;
|
||||
// Render the table initially using the current sortOrder
|
||||
renderFileTable(folder);
|
||||
} else {
|
||||
fileListContainer.textContent = "No files found.";
|
||||
document.getElementById("deleteSelectedBtn").style.display = "none";
|
||||
document.getElementById("copySelectedBtn").style.display = "none";
|
||||
document.getElementById("moveSelectedBtn").style.display = "none";
|
||||
document.getElementById("copyMoveFolderSelect").style.display = "none";
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading file list:", error));
|
||||
}
|
||||
|
||||
// Helper function to escape special HTML characters
|
||||
function escapeHTML(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderFileTable(folder) {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
// Use encodeURIComponent on folder for the URL part
|
||||
const folderPath = (folder === "root") ? "uploads/" : "uploads/" + encodeURIComponent(folder) + "/";
|
||||
let tableHTML = `<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
|
||||
<th data-column="name" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="modified" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="uploaded" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="size" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th data-column="uploader" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
|
||||
Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
fileData.forEach(file => {
|
||||
// Determine if file is editable via your canEditFile() helper
|
||||
const isEditable = canEditFile(file.name);
|
||||
// Escape user-supplied file name and other properties for safe HTML output.
|
||||
const safeFileName = escapeHTML(file.name);
|
||||
const safeModified = escapeHTML(file.modified);
|
||||
const safeUploaded = escapeHTML(file.uploaded);
|
||||
const safeSize = escapeHTML(file.size);
|
||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||
|
||||
tableHTML += `<tr>
|
||||
<td><input type="checkbox" class="file-checkbox" value="${safeFileName}" onclick="toggleDeleteButton()"></td>
|
||||
<td>${safeFileName}</td>
|
||||
<td style="white-space: nowrap;">${safeModified}</td>
|
||||
<td style="white-space: nowrap;">${safeUploaded}</td>
|
||||
<td style="white-space: nowrap;">${safeSize}</td>
|
||||
<td style="white-space: nowrap;">${safeUploader}</td>
|
||||
<td>
|
||||
<div style="display: inline-flex; align-items: center; gap: 5px; flex-wrap: nowrap;">
|
||||
<a class="btn btn-sm btn-success" href="${folderPath + encodeURIComponent(file.name)}" download>Download</a>
|
||||
${isEditable
|
||||
? `<button class="btn btn-sm btn-primary ml-2" onclick="editFile(${JSON.stringify(file.name)}, ${JSON.stringify(folder)})">Edit</button>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
tableHTML += `</tbody></table>`;
|
||||
fileListContainer.innerHTML = tableHTML;
|
||||
|
||||
// Attach click event listeners to header cells for sorting
|
||||
const headerCells = document.querySelectorAll("table.table thead th[data-column]");
|
||||
headerCells.forEach(cell => {
|
||||
cell.addEventListener("click", function () {
|
||||
const column = this.getAttribute("data-column");
|
||||
sortFiles(column, folder);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Show or hide action buttons based on whether files exist
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
const copyBtn = document.getElementById("copySelectedBtn");
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
if (fileData.length > 0) {
|
||||
deleteBtn.style.display = "block";
|
||||
copyBtn.style.display = "block";
|
||||
moveBtn.style.display = "block";
|
||||
document.getElementById("copyMoveFolderSelect").style.display = "inline-block";
|
||||
} else {
|
||||
deleteBtn.style.display = "none";
|
||||
copyBtn.style.display = "none";
|
||||
moveBtn.style.display = "none";
|
||||
document.getElementById("copyMoveFolderSelect").style.display = "none";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function sortFiles(column, folder) {
|
||||
// Toggle sort direction if the same column is clicked; otherwise, sort ascending
|
||||
if (sortOrder.column === column) {
|
||||
sortOrder.ascending = !sortOrder.ascending;
|
||||
} else {
|
||||
sortOrder.column = column;
|
||||
sortOrder.ascending = true;
|
||||
}
|
||||
fileData.sort((a, b) => {
|
||||
let valA = a[column] || "";
|
||||
let valB = b[column] || "";
|
||||
// If sorting by date, convert to timestamp
|
||||
if (column === "modified" || column === "uploaded") {
|
||||
valA = new Date(valA).getTime();
|
||||
valB = new Date(valB).getTime();
|
||||
} else if (typeof valA === "string") {
|
||||
valA = valA.toLowerCase();
|
||||
valB = valB.toLowerCase();
|
||||
}
|
||||
if (valA < valB) return sortOrder.ascending ? -1 : 1;
|
||||
if (valA > valB) return sortOrder.ascending ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
// Re-render the table after sorting
|
||||
renderFileTable(folder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Delete Selected Files handler (existing)
|
||||
function handleDeleteSelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete the selected files?")) {
|
||||
return;
|
||||
}
|
||||
const filesToDelete = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("deleteFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ folder: currentFolder, files: filesToDelete })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files deleted successfully!");
|
||||
loadFileList(currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not delete files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error deleting files:", error));
|
||||
}
|
||||
|
||||
// NEW: Handle Copy Selected Files
|
||||
function handleCopySelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected for copying.");
|
||||
return;
|
||||
}
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder for copying.");
|
||||
return;
|
||||
}
|
||||
const filesToCopy = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("copyFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ source: currentFolder, files: filesToCopy, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files copied successfully!");
|
||||
loadFileList(currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not copy files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error copying files:", error));
|
||||
}
|
||||
|
||||
// NEW: Handle Move Selected Files
|
||||
function handleMoveSelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (checkboxes.length === 0) {
|
||||
alert("No files selected for moving.");
|
||||
return;
|
||||
}
|
||||
const targetFolder = document.getElementById("copyMoveFolderSelect").value;
|
||||
if (!targetFolder) {
|
||||
alert("Please select a target folder for moving.");
|
||||
return;
|
||||
}
|
||||
const filesToMove = Array.from(checkboxes).map(chk => chk.value);
|
||||
fetch("moveFiles.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ source: currentFolder, files: filesToMove, destination: targetFolder })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert("Selected files moved successfully!");
|
||||
loadFileList(currentFolder);
|
||||
} else {
|
||||
alert("Error: " + (data.error || "Could not move files"));
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error moving files:", error));
|
||||
}
|
||||
|
||||
// Attach event listeners to the action buttons.
|
||||
// Use cloneNode() to remove any previously attached listeners.
|
||||
const deleteSelectedBtn = document.getElementById("deleteSelectedBtn");
|
||||
deleteSelectedBtn.replaceWith(deleteSelectedBtn.cloneNode(true));
|
||||
document.getElementById("deleteSelectedBtn").addEventListener("click", handleDeleteSelected);
|
||||
|
||||
const copySelectedBtn = document.getElementById("copySelectedBtn");
|
||||
copySelectedBtn.replaceWith(copySelectedBtn.cloneNode(true));
|
||||
document.getElementById("copySelectedBtn").addEventListener("click", handleCopySelected);
|
||||
|
||||
const moveSelectedBtn = document.getElementById("moveSelectedBtn");
|
||||
moveSelectedBtn.replaceWith(moveSelectedBtn.cloneNode(true));
|
||||
document.getElementById("moveSelectedBtn").addEventListener("click", handleMoveSelected);
|
||||
|
||||
// NEW: Load the folder list into the copy/move dropdown
|
||||
function loadCopyMoveFolderList() {
|
||||
fetch("getFolderList.php")
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const folderSelect = document.getElementById("copyMoveFolderSelect");
|
||||
folderSelect.innerHTML = "";
|
||||
// Optionally, add a default prompt option
|
||||
const defaultOption = document.createElement("option");
|
||||
defaultOption.value = "";
|
||||
defaultOption.textContent = "Select folder";
|
||||
folderSelect.appendChild(defaultOption);
|
||||
if (data && data.length > 0) {
|
||||
data.forEach(folder => {
|
||||
const option = document.createElement("option");
|
||||
option.value = folder;
|
||||
option.textContent = folder;
|
||||
folderSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error loading folder list:", error));
|
||||
}
|
||||
|
||||
// On DOMContentLoaded, load the file list and the folder dropdown.
|
||||
// Ensure currentFolder is defined globally (defaulting to "root" if not).
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
currentFolder = currentFolder || "root";
|
||||
loadFileList(currentFolder);
|
||||
loadCopyMoveFolderList();
|
||||
});
|
||||
|
||||
// -----------------------
|
||||
// File Editing Functions
|
||||
// -----------------------
|
||||
window.editFile = function (fileName, folder) {
|
||||
console.log("Edit button clicked for:", fileName);
|
||||
let existingEditor = document.getElementById("editorContainer");
|
||||
if (existingEditor) { existingEditor.remove(); }
|
||||
const folderUsed = folder || currentFolder || "root";
|
||||
const folderPath = (folderUsed === "root") ? "uploads/" : "uploads/" + encodeURIComponent(folderUsed) + "/";
|
||||
const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime();
|
||||
|
||||
// First, use a HEAD request to check file size
|
||||
fetch(fileUrl, { method: "HEAD" })
|
||||
.then(response => {
|
||||
const contentLength = response.headers.get("Content-Length");
|
||||
if (contentLength && parseInt(contentLength) > 10485760) {
|
||||
alert("This file is larger than 10 MB and cannot be edited in the browser.");
|
||||
throw new Error("File too large.");
|
||||
}
|
||||
// File size is acceptable; now fetch the file content
|
||||
return fetch(fileUrl);
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("HTTP error! Status: " + response.status);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(content => {
|
||||
const modal = document.createElement("div");
|
||||
modal.id = "editorContainer";
|
||||
modal.classList.add("modal", "editor-modal");
|
||||
modal.innerHTML = `
|
||||
<h3>Editing: ${fileName}</h3>
|
||||
<textarea id="fileEditor" style="width:100%; height:80%; resize:none;">${content}</textarea>
|
||||
<div style="margin-top:10px; text-align:right;">
|
||||
<button onclick="saveFile('${fileName}', '${folderUsed}')" class="btn btn-primary">Save</button>
|
||||
<button onclick="document.getElementById('editorContainer').remove()" class="btn btn-secondary">Close</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = "block";
|
||||
})
|
||||
.catch(error => console.error("Error loading file:", error));
|
||||
};
|
||||
|
||||
|
||||
window.saveFile = function (fileName, folder) {
|
||||
const editor = document.getElementById("fileEditor");
|
||||
if (!editor) {
|
||||
console.error("Editor not found!");
|
||||
return;
|
||||
}
|
||||
const folderUsed = folder || currentFolder || "root";
|
||||
const fileDataObj = {
|
||||
fileName: fileName,
|
||||
content: editor.value,
|
||||
folder: folderUsed
|
||||
};
|
||||
fetch("saveFile.php", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(fileDataObj)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
alert(result.success || result.error);
|
||||
document.getElementById("editorContainer")?.remove();
|
||||
loadFileList(folderUsed);
|
||||
})
|
||||
.catch(error => console.error("Error saving file:", error));
|
||||
};
|
||||
|
||||
// -----------------------
|
||||
// Upload Form Handling
|
||||
// -----------------------
|
||||
const fileInput = document.getElementById("file");
|
||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||
const uploadForm = document.getElementById("uploadFileForm");
|
||||
|
||||
fileInput.addEventListener("change", function () {
|
||||
progressContainer.innerHTML = "";
|
||||
const files = fileInput.files;
|
||||
if (files.length > 0) {
|
||||
const list = document.createElement("ul");
|
||||
list.style.listStyle = "none";
|
||||
list.style.padding = "0";
|
||||
Array.from(files).forEach((file, index) => {
|
||||
const listItem = document.createElement("li");
|
||||
listItem.style.paddingTop = "20px";
|
||||
listItem.style.marginBottom = "10px";
|
||||
listItem.style.display = "flex";
|
||||
listItem.style.alignItems = "center";
|
||||
listItem.style.flexWrap = "wrap";
|
||||
const previewContainer = document.createElement("div");
|
||||
previewContainer.className = "file-preview";
|
||||
displayFilePreview(file, previewContainer);
|
||||
const fileNameDiv = document.createElement("div");
|
||||
fileNameDiv.textContent = file.name;
|
||||
fileNameDiv.style.flexGrow = "1";
|
||||
fileNameDiv.style.marginLeft = "5px";
|
||||
fileNameDiv.style.wordBreak = "break-word";
|
||||
const progressDiv = document.createElement("div");
|
||||
progressDiv.classList.add("progress");
|
||||
progressDiv.style.flex = "0 0 250px";
|
||||
progressDiv.style.marginLeft = "5px";
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.classList.add("progress-bar");
|
||||
progressBar.style.width = "0%";
|
||||
progressBar.innerText = "0%";
|
||||
progressDiv.appendChild(progressBar);
|
||||
listItem.appendChild(previewContainer);
|
||||
listItem.appendChild(fileNameDiv);
|
||||
listItem.appendChild(progressDiv);
|
||||
listItem.progressBar = progressBar;
|
||||
listItem.startTime = Date.now();
|
||||
list.appendChild(listItem);
|
||||
});
|
||||
progressContainer.appendChild(list);
|
||||
}
|
||||
});
|
||||
|
||||
uploadForm.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
const files = fileInput.files;
|
||||
if (files.length === 0) {
|
||||
alert("No files selected.");
|
||||
return;
|
||||
}
|
||||
const folderToUse = currentFolder || "root";
|
||||
const listItems = progressContainer.querySelectorAll("li");
|
||||
let finishedCount = 0;
|
||||
Array.from(files).forEach((file, index) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file[]", file);
|
||||
formData.append("folder", folderToUse);
|
||||
const xhr = new XMLHttpRequest();
|
||||
let currentPercent = 0;
|
||||
xhr.upload.addEventListener("progress", function (e) {
|
||||
if (e.lengthComputable) {
|
||||
currentPercent = Math.round((e.loaded / e.total) * 100);
|
||||
const elapsedTime = (Date.now() - listItems[index].startTime) / 1000;
|
||||
let speedText = "";
|
||||
if (elapsedTime > 0) {
|
||||
const speed = e.loaded / elapsedTime;
|
||||
if (speed < 1024) speedText = speed.toFixed(0) + " B/s";
|
||||
else if (speed < 1048576) speedText = (speed / 1024).toFixed(1) + " KB/s";
|
||||
else speedText = (speed / 1048576).toFixed(1) + " MB/s";
|
||||
}
|
||||
listItems[index].progressBar.style.width = currentPercent + "%";
|
||||
listItems[index].progressBar.innerText = currentPercent + "% (" + speedText + ")";
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("load", function () {
|
||||
if (currentPercent >= 100) {
|
||||
listItems[index].progressBar.innerText = "Done";
|
||||
}
|
||||
finishedCount++;
|
||||
console.log("Upload response for file", file.name, xhr.responseText);
|
||||
if (finishedCount === files.length) {
|
||||
loadFileList(folderToUse);
|
||||
fileInput.value = "";
|
||||
setTimeout(() => { progressContainer.innerHTML = ""; }, 5000);
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("error", function () {
|
||||
listItems[index].progressBar.innerText = "Error";
|
||||
});
|
||||
xhr.open("POST", "upload.php", true);
|
||||
xhr.send(formData);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user