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">
|
<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:**
|
**Changes 3/4/2025:**
|
||||||
Copy & Move functionality added
|
Copy & Move functionality added
|
||||||
Header Layout
|
Header Layout
|
||||||
@@ -11,7 +49,6 @@
|
|||||||
CSS Consolidation
|
CSS Consolidation
|
||||||
assets folder
|
assets folder
|
||||||
|
|
||||||
|
|
||||||
**Changes 3/3/2025:**
|
**Changes 3/3/2025:**
|
||||||
folder management added
|
folder management added
|
||||||
some refactoring
|
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.
|
This multi-file uploader with editing and user management is ideal for scenarios involving document management, image galleries, firmware updates, and more.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- **Login Page**
|
- **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">
|
<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:
|
based off of:
|
||||||
https://github.com/sensboston/uploader
|
https://github.com/sensboston/uploader
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Apache2, configured, up and running
|
- Apache2, configured, up and running
|
||||||
@@ -127,4 +158,3 @@ https://github.com/sensboston/uploader
|
|||||||
- Required PHP extensions: `php-json`, `php-curl`
|
- Required PHP extensions: `php-json`, `php-curl`
|
||||||
|
|
||||||
...........
|
...........
|
||||||
|
|
||||||
|
|||||||
168
auth.js
168
auth.js
@@ -1,15 +1,18 @@
|
|||||||
// auth.js
|
// 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 () {
|
export function initAuth() {
|
||||||
// Hide file list and upload form on load.
|
// On initial load, show the login form and hide the main operations.
|
||||||
toggleVisibility("fileListContainer", false);
|
toggleVisibility("loginForm", true);
|
||||||
toggleVisibility("uploadFileForm", false);
|
toggleVisibility("mainOperations", false);
|
||||||
|
// Ensure header buttons are hidden.
|
||||||
checkAuthentication();
|
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||||
|
|
||||||
|
// Set up the authentication form listener.
|
||||||
document.getElementById("authForm").addEventListener("submit", function (event) {
|
document.getElementById("authForm").addEventListener("submit", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const formData = {
|
const formData = {
|
||||||
@@ -22,58 +25,179 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
console.log("Login response:", data);
|
console.log("Login response:", data);
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log("Login successful.");
|
console.log("Login successful.");
|
||||||
|
// On successful login, hide the login form and show main operations.
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
|
toggleVisibility("mainOperations", true);
|
||||||
toggleVisibility("uploadFileForm", true);
|
toggleVisibility("uploadFileForm", true);
|
||||||
toggleVisibility("fileListContainer", 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 {
|
} else {
|
||||||
alert("Login failed: " + (data.error || "Unknown error"));
|
alert("Login failed: " + (data.error || "Unknown error"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error logging in:", 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() {
|
export function checkAuthentication() {
|
||||||
sendRequest("checkAuth.php")
|
sendRequest("checkAuth.php")
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log("Authentication check:", data);
|
console.log("Authentication check:", data);
|
||||||
if (data.setup) {
|
if (data.setup) {
|
||||||
setupMode = true;
|
window.setupMode = true;
|
||||||
// In setup mode, hide all sections except the Add User modal.
|
// In setup mode, hide login and main operations; show Add User modal.
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
toggleVisibility("uploadFileForm", false);
|
toggleVisibility("mainOperations", false);
|
||||||
toggleVisibility("fileListContainer", false);
|
|
||||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||||
toggleVisibility("addUserModal", true);
|
toggleVisibility("addUserModal", true);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
setupMode = false;
|
window.setupMode = false;
|
||||||
}
|
}
|
||||||
if (data.authenticated) {
|
if (data.authenticated) {
|
||||||
toggleVisibility("loginForm", false);
|
toggleVisibility("loginForm", false);
|
||||||
|
toggleVisibility("mainOperations", true);
|
||||||
toggleVisibility("uploadFileForm", true);
|
toggleVisibility("uploadFileForm", true);
|
||||||
toggleVisibility("fileListContainer", true);
|
toggleVisibility("fileListContainer", true);
|
||||||
if (typeof loadFileList === "function") {
|
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||||
loadFileList();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
toggleVisibility("loginForm", true);
|
toggleVisibility("loginForm", true);
|
||||||
|
toggleVisibility("mainOperations", false);
|
||||||
toggleVisibility("uploadFileForm", false);
|
toggleVisibility("uploadFileForm", false);
|
||||||
toggleVisibility("fileListContainer", false);
|
toggleVisibility("fileListContainer", false);
|
||||||
|
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => console.error("Error checking authentication:", error));
|
.catch(error => console.error("Error checking authentication:", error));
|
||||||
}
|
}
|
||||||
window.checkAuthentication = checkAuthentication;
|
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() {
|
function closeAddUserModal() {
|
||||||
toggleVisibility("addUserModal", false);
|
toggleVisibility("addUserModal", false);
|
||||||
resetUserForm();
|
resetUserForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetUserForm() {
|
function closeRemoveUserModal() {
|
||||||
document.getElementById("newUsername").value = "";
|
toggleVisibility("removeUserModal", false);
|
||||||
document.getElementById("newPassword").value = "";
|
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
|
<?php
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
session_start();
|
session_start();
|
||||||
|
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||||
|
header("Pragma: no-cache");
|
||||||
|
header("Expires: 0");
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
|
|||||||
@@ -127,9 +127,7 @@
|
|||||||
|
|
||||||
<!-- JavaScript Files -->
|
<!-- JavaScript Files -->
|
||||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
<script type="module" src="utils.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
<script type="module" src="auth.js"></script>
|
|
||||||
<script type="module" src="upload.js"></script>
|
|
||||||
<script type="module" src="displayFileList.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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"]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
59
styles.css
59
styles.css
@@ -33,7 +33,12 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.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;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -64,6 +69,7 @@ header {
|
|||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -86,7 +92,8 @@ header {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(75%, 75%); /* centers the modal */
|
transform: translate(75%, 75%);
|
||||||
|
/* centers the modal */
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
@@ -98,11 +105,13 @@ header {
|
|||||||
height: 600px;
|
height: 600px;
|
||||||
max-height: 35vh;
|
max-height: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-modal {
|
.editor-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(5%, 10%); /* centers the editor modal */
|
transform: translate(5%, 10%);
|
||||||
|
/* centers the editor modal */
|
||||||
width: 50vw;
|
width: 50vw;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
@@ -129,28 +138,34 @@ header {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer li {
|
#uploadProgressContainer li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer .file-preview {
|
#uploadProgressContainer .file-preview {
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
height: 32px !important;
|
height: 32px !important;
|
||||||
margin-right: 1px;
|
margin-right: 0px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer .file-preview img {
|
#uploadProgressContainer .file-preview img {
|
||||||
width: 32px !important;
|
width: 32px !important;
|
||||||
height: 32px !important;
|
height: 32px !important;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer .file-name {
|
#uploadProgressContainer .file-name {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
|
margin-left: 2px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer .progress {
|
#uploadProgressContainer .progress {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -160,6 +175,7 @@ header {
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer .progress-bar {
|
#uploadProgressContainer .progress-bar {
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -169,6 +185,7 @@ header {
|
|||||||
transition: width 0.4s ease;
|
transition: width 0.4s ease;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#uploadProgressContainer {
|
#uploadProgressContainer {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
@@ -181,10 +198,16 @@ header {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-container button {
|
.logout-container button {
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hide-small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BUTTON STYLES (MATERIAL THEME) */
|
/* BUTTON STYLES (MATERIAL THEME) */
|
||||||
@@ -198,9 +221,11 @@ header {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File list action buttons (for Delete, Copy, Move) */
|
/* File list action buttons (for Delete, Copy, Move) */
|
||||||
.file-list-actions button {
|
.file-list-actions button {
|
||||||
background-color: #2196F3;
|
background-color: #2196F3;
|
||||||
@@ -211,12 +236,14 @@ header {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-actions button:hover {
|
.file-list-actions button:hover {
|
||||||
background-color: #1976D2;
|
background-color: #1976D2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#deleteSelectedBtn {
|
#deleteSelectedBtn {
|
||||||
background-color: #f44336; /* Material red */
|
background-color: #f44336;
|
||||||
|
/* Material red */
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -228,7 +255,8 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#copySelectedBtn {
|
#copySelectedBtn {
|
||||||
background-color: #9E9E9E; /* Material grey */
|
background-color: #9E9E9E;
|
||||||
|
/* Material grey */
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -240,7 +268,8 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#moveSelectedBtn {
|
#moveSelectedBtn {
|
||||||
background-color: #ff9800; /* Material orange */
|
background-color: #ff9800;
|
||||||
|
/* Material orange */
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -261,6 +290,7 @@ header {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileList button.edit-btn:hover {
|
#fileList button.edit-btn:hover {
|
||||||
background-color: #43A047;
|
background-color: #43A047;
|
||||||
}
|
}
|
||||||
@@ -273,6 +303,7 @@ header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-dropdown {
|
.folder-dropdown {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
@@ -282,15 +313,20 @@ header {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileList table th,
|
#fileList table th,
|
||||||
#fileList table td {
|
#fileList table td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: none; /* Remove table borders */
|
border: none;
|
||||||
|
/* Remove table borders */
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileList table tr:nth-child(even) {
|
#fileList table tr:nth-child(even) {
|
||||||
background-color: transparent; /* Remove alternating grey rows */
|
background-color: transparent;
|
||||||
|
/* Remove alternating grey rows */
|
||||||
}
|
}
|
||||||
|
|
||||||
#fileList table tr:hover {
|
#fileList table tr:hover {
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
}
|
||||||
@@ -299,6 +335,7 @@ header {
|
|||||||
h2 {
|
h2 {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -312,6 +349,7 @@ label {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th button {
|
.table th button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -321,7 +359,8 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* INITIAL HIDE FORMS */
|
/* INITIAL HIDE FORMS */
|
||||||
#loginForm, #uploadForm {
|
#loginForm,
|
||||||
|
#uploadForm {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
232
upload.js
232
upload.js
@@ -1,83 +1,77 @@
|
|||||||
// upload.js
|
// 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 fileInput = document.getElementById("file");
|
||||||
const progressContainer = document.getElementById("uploadProgressContainer");
|
const progressContainer = document.getElementById("uploadProgressContainer");
|
||||||
const uploadForm = document.getElementById("uploadFileForm");
|
const uploadForm = document.getElementById("uploadFileForm");
|
||||||
|
|
||||||
function updateUploadProgress(e, listItem) {
|
// Build progress list when files are selected.
|
||||||
if (e.lengthComputable) {
|
if (fileInput) {
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listItem.progressBar.style.width = currentPercent + "%";
|
|
||||||
listItem.progressBar.innerText = currentPercent + "% (" + speedText + ")";
|
|
||||||
return currentPercent;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", function () {
|
fileInput.addEventListener("change", function () {
|
||||||
progressContainer.innerHTML = "";
|
progressContainer.innerHTML = "";
|
||||||
const files = fileInput.files;
|
const files = fileInput.files;
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
|
const allFiles = Array.from(files);
|
||||||
|
const maxDisplay = 10;
|
||||||
const list = document.createElement("ul");
|
const list = document.createElement("ul");
|
||||||
list.style.listStyle = "none";
|
list.style.listStyle = "none";
|
||||||
list.style.padding = "0";
|
list.style.padding = "0";
|
||||||
Array.from(files).forEach((file) => {
|
allFiles.forEach((file, index) => {
|
||||||
const listItem = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
listItem.style.paddingTop = "20px";
|
li.style.paddingTop = "20px";
|
||||||
listItem.style.marginBottom = "10px";
|
li.style.marginBottom = "10px";
|
||||||
listItem.style.display = "flex";
|
// Only display progress items for the first maxDisplay files.
|
||||||
listItem.style.alignItems = "center";
|
li.style.display = (index < maxDisplay) ? "flex" : "none";
|
||||||
listItem.style.flexWrap = "wrap";
|
li.style.alignItems = "center";
|
||||||
|
li.style.flexWrap = "wrap";
|
||||||
|
|
||||||
const previewContainer = document.createElement("div");
|
const preview = document.createElement("div");
|
||||||
previewContainer.className = "file-preview";
|
preview.className = "file-preview";
|
||||||
displayFilePreview(file, previewContainer);
|
displayFilePreview(file, preview);
|
||||||
|
|
||||||
const fileNameDiv = document.createElement("div");
|
const nameDiv = document.createElement("div");
|
||||||
fileNameDiv.textContent = file.name;
|
nameDiv.textContent = file.name;
|
||||||
fileNameDiv.style.flexGrow = "1";
|
nameDiv.style.flexGrow = "1";
|
||||||
fileNameDiv.style.marginLeft = "5px";
|
nameDiv.style.marginLeft = "5px";
|
||||||
fileNameDiv.style.wordBreak = "break-word";
|
nameDiv.style.wordBreak = "break-word";
|
||||||
|
|
||||||
const progressDiv = document.createElement("div");
|
const progDiv = document.createElement("div");
|
||||||
progressDiv.classList.add("progress");
|
progDiv.classList.add("progress");
|
||||||
progressDiv.style.flex = "0 0 250px";
|
progDiv.style.flex = "0 0 250px";
|
||||||
progressDiv.style.marginLeft = "5px";
|
progDiv.style.marginLeft = "5px";
|
||||||
|
|
||||||
const progressBar = document.createElement("div");
|
const progBar = document.createElement("div");
|
||||||
progressBar.classList.add("progress-bar");
|
progBar.classList.add("progress-bar");
|
||||||
progressBar.style.width = "0%";
|
progBar.style.width = "0%";
|
||||||
progressBar.innerText = "0%";
|
progBar.innerText = "0%";
|
||||||
|
|
||||||
progressDiv.appendChild(progressBar);
|
progDiv.appendChild(progBar);
|
||||||
|
li.appendChild(preview);
|
||||||
listItem.appendChild(previewContainer);
|
li.appendChild(nameDiv);
|
||||||
listItem.appendChild(fileNameDiv);
|
li.appendChild(progDiv);
|
||||||
listItem.appendChild(progressDiv);
|
// Save references for later updates.
|
||||||
|
li.progressBar = progBar;
|
||||||
listItem.progressBar = progressBar;
|
li.startTime = Date.now();
|
||||||
listItem.startTime = Date.now();
|
list.appendChild(li);
|
||||||
|
|
||||||
list.appendChild(listItem);
|
|
||||||
});
|
});
|
||||||
|
// 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);
|
progressContainer.appendChild(list);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit handler: upload all files and then check the server's file list.
|
||||||
|
if (uploadForm) {
|
||||||
uploadForm.addEventListener("submit", function (e) {
|
uploadForm.addEventListener("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const files = fileInput.files;
|
const files = fileInput.files;
|
||||||
@@ -85,45 +79,131 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
alert("No files selected.");
|
alert("No files selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
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");
|
const listItems = progressContainer.querySelectorAll("li");
|
||||||
let finishedCount = 0;
|
let finishedCount = 0;
|
||||||
|
let allSucceeded = true;
|
||||||
|
// Array to track each file's upload result.
|
||||||
|
const uploadResults = new Array(allFiles.length).fill(false);
|
||||||
|
|
||||||
Array.from(files).forEach((file, index) => {
|
allFiles.forEach((file, index) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file[]", file);
|
formData.append("file[]", file);
|
||||||
const folderElem = document.getElementById("folderSelect");
|
formData.append("folder", folderToUse);
|
||||||
if (folderElem) {
|
|
||||||
formData.append("folder", folderElem.value);
|
|
||||||
} else {
|
|
||||||
console.error("Folder selection element not found!");
|
|
||||||
}
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
let currentPercent = 0;
|
let currentPercent = 0;
|
||||||
|
|
||||||
xhr.upload.addEventListener("progress", function (e) {
|
xhr.upload.addEventListener("progress", function (e) {
|
||||||
currentPercent = updateUploadProgress(e, listItems[index]);
|
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 + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener("load", function () {
|
xhr.addEventListener("load", function () {
|
||||||
if (currentPercent >= 100) {
|
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";
|
listItems[index].progressBar.innerText = "Done";
|
||||||
}
|
}
|
||||||
|
uploadResults[index] = true;
|
||||||
|
} else {
|
||||||
|
if (index < maxDisplay && listItems[index]) {
|
||||||
|
listItems[index].progressBar.innerText = "Error";
|
||||||
|
}
|
||||||
|
allSucceeded = false;
|
||||||
|
}
|
||||||
finishedCount++;
|
finishedCount++;
|
||||||
console.log("Upload response for file", file.name, xhr.responseText);
|
console.log("Upload response for file", file.name, xhr.responseText);
|
||||||
if (finishedCount === files.length) {
|
if (finishedCount === allFiles.length) {
|
||||||
if (typeof loadFileList === "function") {
|
// Immediately refresh the file list.
|
||||||
loadFileList();
|
refreshFileList();
|
||||||
}
|
|
||||||
fileInput.value = "";
|
|
||||||
setTimeout(() => {
|
|
||||||
progressContainer.innerHTML = "";
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener("error", function () {
|
xhr.addEventListener("error", function () {
|
||||||
|
if (index < maxDisplay && listItems[index]) {
|
||||||
listItems[index].progressBar.innerText = "Error";
|
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.open("POST", "upload.php", true);
|
||||||
xhr.send(formData);
|
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