new features and refactor

This commit is contained in:
Ryan
2025-03-07 03:22:20 -05:00
committed by GitHub
parent 6a41280667
commit 960b27b414
14 changed files with 1338 additions and 1225 deletions

View File

@@ -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
View File

@@ -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));
}

View File

@@ -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
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 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
View 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
View 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));
}
});

View File

@@ -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) {

View File

@@ -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
View 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
View 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
View 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"]);
}
?>

View File

@@ -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
View File

@@ -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
View File

@@ -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 elements 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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);
});
});
});