Copy/Move functionality

This commit is contained in:
Ryan
2025-03-04 06:55:36 -05:00
committed by GitHub
parent 850e42af15
commit 8d05a36f04
7 changed files with 650 additions and 487 deletions

View File

@@ -3,13 +3,19 @@
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/main-screen.png" alt="main screen">
**Changes 3/3/2025:**
**Changes 3/4/2025:**
Copy & Move functionality added
Header Layout
Modal Popups (Edit, Add User, Remove User) changes
Consolidated table styling
CSS Consolidation
assets folder
folder management added
some refactoring
config added USERS_DIR & USERS_FILE
**Changes 3/3/2025:**
folder management added
some refactoring
config added USERS_DIR & USERS_FILE
# Multi File Upload & Edit
@@ -110,15 +116,10 @@ This project is a lightweight, secure web application for uploading, editing, an
fork of:
based off of:
https://github.com/sensboston/uploader
# File Uploader
A simple file uploader web app that allows authenticated users to upload, list, and delete files.
The application uses PHP, running on Apache2, Ubuntu (but definitely should work everywhere).
## Prerequisites
- Apache2, configured, up and running

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,8 +1,10 @@
// 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")
@@ -13,7 +15,7 @@ export function loadFileList() {
return;
}
toggleVisibility("fileListContainer", true);
return sendRequest("getFileList.php");
return sendRequest("getFileList.php?folder=" + encodeURIComponent(currentFolder));
})
.then(data => {
if (!data) return;
@@ -26,111 +28,23 @@ export function loadFileList() {
return;
}
fileData = data.files;
sortFiles("uploaded", false);
//sortFiles("uploaded", false);
})
.catch(error => console.error("Error loading file list:", error));
}
export function sortFiles(column, forceAscending = null) {
if (sortOrder.column === column) {
sortOrder.ascending = forceAscending !== null ? forceAscending : !sortOrder.ascending;
} else {
sortOrder.column = column;
sortOrder.ascending = forceAscending !== null ? forceAscending : true;
}
fileData.sort((a, b) => {
let valA = a[column] || "";
let valB = b[column] || "";
if (column === "modified" || column === "uploaded") {
const dateA = new Date(valA);
const dateB = new Date(valB);
if (!isNaN(dateA.getTime()) && !isNaN(dateB.getTime())) {
valA = dateA.getTime();
valB = dateB.getTime();
} else {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
} 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();
}
export function renderFileTable() {
const fileListContainer = document.getElementById("fileList");
let tableHTML = `<table class="table">
<thead>
<tr>
<th><input type="checkbox" id="selectAll" onclick="toggleAllCheckboxes(this)"></th>
<th onclick="sortFiles('name')" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
<span>File Name</span> <span>${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}</span>
</th>
<th onclick="sortFiles('modified')" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
<span>Date Modified</span> <span>${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}</span>
</th>
<th onclick="sortFiles('uploaded')" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
<span>Upload Date</span> <span>${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}</span>
</th>
<th onclick="sortFiles('size')" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
<span>File Size</span> <span>${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}</span>
</th>
<th onclick="sortFiles('uploader')" style="cursor:pointer; text-decoration: underline; white-space: nowrap;">
<span>Uploader</span> <span>${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}</span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>`;
fileData.forEach(file => {
const isEditable = file.name.endsWith(".txt") || file.name.endsWith(".json") ||
file.name.endsWith(".ini") || file.name.endsWith(".css") ||
file.name.endsWith(".js") || file.name.endsWith(".csv") ||
file.name.endsWith(".md") || file.name.endsWith(".xml") ||
file.name.endsWith(".html") || file.name.endsWith(".py") ||
file.name.endsWith(".log") || file.name.endsWith(".conf") ||
file.name.endsWith(".config") || file.name.endsWith(".bat") ||
file.name.endsWith(".rtf") || file.name.endsWith(".doc") ||
file.name.endsWith(".docx");
tableHTML += `<tr>
<td><input type="checkbox" class="file-checkbox" value="${file.name}" onclick="toggleDeleteButton()"></td>
<td>${file.name}</td>
<td style="white-space: nowrap;">${file.modified}</td>
<td style="white-space: nowrap;">${file.uploaded}</td>
<td style="white-space: nowrap;">${file.size}</td>
<td style="white-space: nowrap;">${file.uploader || "Unknown"}</td>
<td>
<div style="display: inline-flex; align-items: center; gap: 5px; flex-wrap: nowrap;">
<a href="uploads/${file.name}" download>Download</a>
${isEditable ? `<button onclick="editFile('${file.name}')">Edit</button>` : ""}
</div>
</td>
</tr>`;
});
tableHTML += `</tbody></table>`;
fileListContainer.innerHTML = tableHTML;
const deleteBtn = document.getElementById("deleteSelectedBtn");
if (fileData.length > 0) {
deleteBtn.style.display = "block";
const selectedFiles = document.querySelectorAll(".file-checkbox:checked");
deleteBtn.disabled = selectedFiles.length === 0;
} else {
deleteBtn.style.display = "none";
}
}
export function toggleDeleteButton() {
const selectedFiles = document.querySelectorAll(".file-checkbox:checked");
const deleteBtn = document.getElementById("deleteSelectedBtn");
deleteBtn.disabled = selectedFiles.length === 0;
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) {
@@ -141,7 +55,7 @@ export function toggleAllCheckboxes(source) {
export function deleteSelectedFiles() {
const selectedFiles = Array.from(document.querySelectorAll(".file-checkbox:checked"))
.map(checkbox => checkbox.value);
.map(checkbox => checkbox.value);
if (selectedFiles.length === 0) {
alert("No files selected for deletion.");
return;
@@ -158,10 +72,22 @@ export function deleteSelectedFiles() {
}
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);
}
});
export function editFile(fileName) {
@@ -186,12 +112,12 @@ export function editFile(fileName) {
modal.id = "editorContainer";
modal.classList.add("modal", "editor-modal");
modal.innerHTML = `
<h3>Editing: ${fileName}</h3>
<textarea id="fileEditor" style="width:100%; height:60%; resize:none;">${content}</textarea>
<div style="margin-top:10px; text-align:right;">
<button onclick="saveFile('${fileName}')" class="btn btn-primary">Save</button>
<button onclick="document.getElementById('editorContainer').remove()" class="btn btn-secondary">Close</button>
</div>
<h3>Editing: ${fileName}</h3>
<textarea id="fileEditor" style="width:100%; height:60%; resize:none;">${content}</textarea>
<div style="margin-top:10px; text-align:right;">
<button onclick="saveFile('${fileName}')" 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";
@@ -218,11 +144,91 @@ export function saveFile(fileName) {
.catch(error => console.error("Error saving file:", error));
}
// To support inline onclick attributes in the generated HTML, attach these functions to window.
window.sortFiles = sortFiles;
// ===== 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;
}
// 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;
}
console.log("Payload:", {
source: currentFolder,
destination: document.getElementById("copyMoveFolderSelect").value,
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.editFile = editFile;
window.saveFile = saveFile;
window.loadFileList = loadFileList;
window.copySelectedFiles = copySelectedFiles;
window.moveSelectedFiles = moveSelectedFiles;

View File

@@ -2,162 +2,33 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi File Upload & Edit</title>
<link rel="icon" type="image/svg+xml" href="logo.svg">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="shortcut icon" href="logo.svg">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Multi File Upload Editor</title>
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<!-- External CSS -->
<link rel="stylesheet" href="styles.css">
<!-- Google Fonts and Material Icons -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<!-- Scripts (order is important) -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="module" src="utils.js"></script>
<script type="module" src="auth.js"></script>
<script type="module" src="upload.js"></script>
<script type="module" src="displayFileList.js"></script>
<style>
/* General styles */
body {
font-family: 'Roboto', sans-serif;
background-color: #f5f5f5;
}
.container {
margin-top: 30px;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #2196F3;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
min-height: 80px;
}
.header-left,
.header-buttons {
width: 150px;
}
.header-title {
flex: 1;
text-align: center;
}
.header-title h1 {
margin: 0;
font-weight: 500;
color: white;
}
.header-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
align-items: center;
}
.header-buttons button {
background: none;
border: none;
cursor: pointer;
padding: 10px;
border-radius: 50%;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
.header-buttons button:hover {
background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.material-icons {
font-size: 24px;
vertical-align: middle;
color: white;
}
#loginForm {
margin: 0 auto;
max-width: 400px;
background: white;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
width: 350px;
max-width: 90%;
height: auto;
}
.editor-modal {
width: 80vw;
max-width: 90vw;
min-width: 400px;
height: 600px;
max-height: 80vh;
overflow: auto;
resize: both;
}
table.table th {
cursor: pointer;
text-decoration: underline;
white-space: nowrap;
}
.container {
margin-top: 20px;
}
.progress {
background-color: #e9ecef;
border-radius: 5px;
overflow: hidden;
margin-bottom: 10px;
height: 20px;
}
.progress-bar {
background-color: #007bff;
height: 100%;
line-height: 20px;
color: #fff;
text-align: center;
transition: width 0.4s ease;
}
.card {
margin-bottom: 20px;
}
.actions-cell {
white-space: nowrap;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="header-left">
<img src="logo.svg" alt="Filing Cabinet Logo" style="height: 60px; width: auto;">
<img src="/assets/logo.svg" alt="Filing Cabinet Logo">
</div>
<div class="header-title">
<h1>Multi File Upload & Edit</h1>
<h1>Multi File Upload Editor</h1>
</div>
<div class="header-buttons">
<button id="logoutBtn" title="Logout" style="display: none;">
<i class="material-icons">exit_to_app</i>
</button>
<button id="addUserBtn" title="Add User" style="display: none;">
<i class="material-icons">person_add</i>
</button>
<button id="removeUserBtn" title="Remove User" style="display: none;">
<i class="material-icons">person_remove</i>
</button>
<button id="logoutBtn" title="Logout"><i class="material-icons">exit_to_app</i></button>
<button id="addUserBtn" title="Add User"><i class="material-icons">person_add</i></button>
<button id="removeUserBtn" title="Remove User"><i class="material-icons">person_remove</i></button>
</div>
</header>
<div class="container">
<!-- Login Form -->
<div class="row" id="loginForm">
@@ -175,6 +46,7 @@
</form>
</div>
</div>
<!-- Main Operations: Upload and Folder Management -->
<div id="mainOperations" style="display: none;">
<div class="row" id="uploadFolderRow">
@@ -199,8 +71,7 @@
<div class="card flex-fill">
<div class="card-header">Folder Management</div>
<div class="card-body">
<button id="createFolderBtn" class="btn btn-primary mb-3">Create Folder</button>
<div class="form-group d-flex align-items-center">
<div class="form-group d-flex align-items-center" style="padding-top:15px;">
<select id="folderSelect" class="form-control"></select>
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
<i class="material-icons">edit</i>
@@ -209,17 +80,26 @@
<i class="material-icons">delete</i>
</button>
</div>
<button id="createFolderBtn" class="btn btn-primary mt-3">Create Folder</button>
</div>
</div>
</div>
</div>
<!-- File List Section -->
<div id="fileListContainer">
<h2 id="fileListTitle">Files in (Root)</h2>
<button id="deleteSelectedBtn" class="btn btn-danger" style="margin-bottom: 10px; display: none;">Delete Selected</button>
<div id="fileListActions" class="file-list-actions">
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Selected</button>
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Selected</button>
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Selected</button>
<select id="copyMoveFolderSelect" class="form-control folder-dropdown"></select>
</div>
<div id="fileList"></div>
</div>
</div>
<!-- Add User Modal -->
<div id="addUserModal" class="modal">
<h3>Create New User</h3>
@@ -234,6 +114,7 @@
<button id="saveUserBtn" class="btn btn-primary">Save User</button>
<button id="cancelUserBtn" class="btn btn-secondary">Cancel</button>
</div>
<!-- Remove User Modal -->
<div id="removeUserModal" class="modal">
<h3>Remove User</h3>
@@ -243,5 +124,12 @@
<button id="cancelRemoveUserBtn" class="btn btn-secondary">Cancel</button>
</div>
</div>
<!-- JavaScript Files -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script type="module" src="utils.js"></script>
<script type="module" src="auth.js"></script>
<script type="module" src="upload.js"></script>
<script type="module" src="displayFileList.js"></script>
</body>
</html>

View File

@@ -1,20 +1,130 @@
/* Container */
/* GENERAL STYLES */
body {
font-family: 'Roboto', sans-serif;
background-color: #f5f5f5;
margin: 0;
}
/* CONTAINER */
.container {
margin-top: 20px; /* Increased for better spacing */
margin-top: 20px;
}
/* Logout & Add User button container */
/* HEADER */
header {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
height: 100px !important;
padding: 0 20px !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
background-color: #2196F3 !important;
}
.header-left {
flex: 0 0 auto;
}
.header-left img {
display: block;
height: 60px;
width: auto;
}
.header-title {
flex: 1 1 auto;
text-align: center;
margin: 0;
color: white;
font-size: 1.5em;
}
.header-title h1 {
margin: 0;
}
.header-buttons {
flex: 0 0 auto;
display: flex;
gap: 10px;
align-items: center;
}
.header-buttons button {
background: none;
border: none;
cursor: pointer;
padding: 10px;
border-radius: 50%;
transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
.header-buttons button:hover {
background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.material-icons {
font-size: 24px;
vertical-align: middle;
color: white;
}
/* LOGIN FORM */
#loginForm {
margin: 0 auto;
max-width: 400px;
background: white;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* MODALS & EDITOR MODALS */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(75%, 75%); /* centers the modal */
background: white;
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
width: 40vw;
max-width: 40vw;
height: 600px;
max-height: 35vh;
}
.editor-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(5%, 10%); /* centers the editor modal */
width: 50vw;
max-width: 90vw;
min-width: 400px;
height: 600px;
max-height: 80vh;
overflow: auto;
resize: both;
}
/* LOGOUT & USER BUTTON CONTAINER */
.logout-container {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
align-items: flex-end; /* keep buttons right-aligned */
gap: 5px;
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
}
/* Progress bar container style */
/* UPLOAD PROGRESS */
#uploadProgressContainer ul {
list-style: none;
padding: 0;
@@ -24,35 +134,24 @@
display: flex;
align-items: center;
margin-bottom: 10px;
/* Allow wrapping for long file names */
flex-wrap: wrap;
}
/* Force file preview container to be 32x32 pixels and adjust vertical position */
#uploadProgressContainer .file-preview {
width: 32px !important;
height: 32px !important;
margin-right: 10px;
flex-shrink: 0;
/* Use transform to nudge down the preview */
transform: translateY(10px);
}
/* Ensure that the image inside the preview fills the container */
#uploadProgressContainer .file-preview img {
width: 32px !important;
height: 32px !important;
object-fit: cover;
}
/* File name styling */
#uploadProgressContainer .file-name {
margin-right: 20px;
flex-grow: 1;
word-break: break-word;
}
/* Progress bar container style */
#uploadProgressContainer .progress {
background-color: #e9ecef;
border-radius: 5px;
@@ -60,172 +159,187 @@
margin-top: 5px;
margin-bottom: 10px;
height: 24px;
width: 250px; /* Increased width */
/* No extra left margin so it sits right next to file name */
width: 250px;
}
/* Progress bar element style */
#uploadProgressContainer .progress-bar {
background-color: #007bff;
height: 100%;
line-height: 24px;
color: #000; /* black text for legibility */
color: #000;
text-align: center;
transition: width 0.4s ease;
font-size: 0.9rem;
}
/* Ensure the upload progress container has some top margin */
#uploadProgressContainer {
margin-top: 20px;
}
/* On small screens, move buttons below title */
/* RESPONSIVE (small screens) */
@media (max-width: 768px) {
.logout-container {
position: static;
align-items: flex-end; /* Right-align buttons */
text-align: right;
margin-top: 10px;
}
.logout-container button {
width: auto;
min-width: 120px; /* Ensures buttons don't become huge */
}
.logout-container {
position: static;
align-items: flex-end;
text-align: right;
margin-top: 10px;
}
.logout-container button {
width: auto;
min-width: 120px;
}
}
/* Modal styling */
.modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 15px;
border: 1px solid #ccc; /* Lighter border for a modern look */
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
width: 280px; /* slightly narrower */
height: auto; /* Let height adjust to content */
display: flex;
flex-direction: column;
justify-content: space-between;
/* BUTTON STYLES (MATERIAL THEME) */
.btn {
font-size: 0.9rem;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
white-space: nowrap;
text-decoration: none;
display: inline-block;
}
/* Editor modal: for edit popup with rounded corners */
.editor-modal {
width: 80vw;
max-width: 90vw;
min-width: 400px;
height: 400px;
max-height: 80vh;
overflow: auto;
resize: both;
.btn:hover {
opacity: 0.9;
}
/* Header styling */
header {
/* File list action buttons (for Delete, Copy, Move) */
.file-list-actions button {
background-color: #2196F3;
color: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
min-height: 80px;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
.file-list-actions button:hover {
background-color: #1976D2;
}
/* Material icons */
.material-icons {
font-size: 24px;
vertical-align: middle;
#deleteSelectedBtn {
background-color: #f44336; /* Material red */
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
}
/* Header buttons container */
.header-buttons {
#deleteSelectedBtn:hover {
background-color: #d32f2f;
}
#copySelectedBtn {
background-color: #9E9E9E; /* Material grey */
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
}
#copySelectedBtn:hover {
background-color: #757575;
}
#moveSelectedBtn {
background-color: #ff9800; /* Material orange */
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
}
#moveSelectedBtn:hover {
background-color: #fb8c00;
}
/* Material green style for Edit button in file list */
#fileList button.edit-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
}
#fileList button.edit-btn:hover {
background-color: #43A047;
}
/* FILE LIST ACTIONS CONTAINER */
.file-list-actions {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
max-width: 800px;
}
.folder-dropdown {
width: 100px;
}
/* Upload and Choose File button styles */
.btn-upload {
background-color: #007bff;
color: white;
border-radius: 5px;
/* FILE LIST TABLE */
#fileList table {
width: 100%;
border-collapse: collapse;
}
.btn-upload:disabled {
background-color: gray;
#fileList table th,
#fileList table td {
padding: 10px;
text-align: left;
border: none; /* Remove table borders */
}
.btn-choose-file {
background-color: #6c757d;
color: white;
border-radius: 5px;
#fileList table tr:nth-child(even) {
background-color: transparent; /* Remove alternating grey rows */
}
#fileList table tr:hover {
background-color: #e0e0e0;
}
/* File list and progress bar */
.file-list {
margin-top: 10px;
}
.progress {
margin-top: 10px;
height: 20px; /* Narrow progress bar */
width: 100%;
}
.progress-bar {
height: 100%; /* Fill the entire height */
}
/* Table styling */
table {
width: 100%;
border-collapse: collapse;
}
table, th, td {
border: 1px solid #ccc; /* Lighter border color */
}
th, td {
padding: 10px;
text-align: left;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
/* Headings and form labels */
/* HEADINGS & FORM LABELS */
h2 {
font-size: 2em; /* Larger heading for better emphasis */
font-size: 2em;
}
.form-group {
margin-bottom: 10px; /* Increased spacing */
margin-bottom: 10px;
}
label {
font-size: 0.9rem; /* Rem unit for scalability */
font-size: 0.9rem;
}
/* Button font size */
.btn {
font-size: 0.9rem;
}
/* Utility class for aligning items */
/* UTILITY CLASSES */
.align-items-center {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
/* Table header button style (if using buttons inside headers) */
.table th button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 0;
}
/* Initially hide login and upload forms */
/* INITIAL HIDE FORMS */
#loginForm, #uploadForm {
display: none;
display: none;
}
/* Remove bottom margin from the form group */
.card-body .form-group {
margin-bottom: 5px !important;
}
/* Force a small top margin for the Create Folder button */
#createFolderBtn {
margin-top: 0px !important;
}
#fileListContainer {
margin-top: 40px !important;
}

320
utils.js
View File

@@ -1,6 +1,8 @@
// =======================
// Utility Functions
// =======================
let fileData = []; // will store the fetched file data
let sortOrder = { column: "uploaded", ascending: true };
/**
* Sends an AJAX request using the Fetch API.
@@ -61,8 +63,9 @@ let setupMode = false;
* @param {string} fileName
* @returns {boolean}
*/
function canEditFile(fileName) {
const allowedExtensions = ["txt", "html", "htm", "php", "css", "js", "json", "xml", "md", "py"];
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();
@@ -406,6 +409,8 @@ document.addEventListener("DOMContentLoaded", function () {
// -----------------------
// 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))
@@ -414,102 +419,145 @@ document.addEventListener("DOMContentLoaded", function () {
const fileListContainer = document.getElementById("fileList");
fileListContainer.innerHTML = "";
if (data.files && data.files.length > 0) {
const table = document.createElement("table");
table.classList.add("table");
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
// Add select-all checkbox in header.
const selectTh = document.createElement("th");
const selectAll = document.createElement("input");
selectAll.type = "checkbox";
selectAll.id = "selectAllFiles";
selectAll.addEventListener("change", function () {
const checkboxes = document.querySelectorAll(".file-checkbox");
checkboxes.forEach(chk => chk.checked = this.checked);
updateDeleteSelectedVisibility();
});
selectTh.appendChild(selectAll);
headerRow.appendChild(selectTh);
["Name", "Modified", "Uploaded", "Size", "Uploader", "Actions"].forEach(headerText => {
const th = document.createElement("th");
th.textContent = headerText;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
const folderPath = (folder === "root") ? "uploads/" : "uploads/" + encodeURIComponent(folder) + "/";
data.files.forEach(file => {
const row = document.createElement("tr");
const checkboxTd = document.createElement("td");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "file-checkbox";
checkbox.value = file.name;
checkbox.addEventListener("change", updateDeleteSelectedVisibility);
checkboxTd.appendChild(checkbox);
row.appendChild(checkboxTd);
const nameTd = document.createElement("td");
nameTd.textContent = file.name;
row.appendChild(nameTd);
const modifiedTd = document.createElement("td");
modifiedTd.textContent = file.modified;
row.appendChild(modifiedTd);
const uploadedTd = document.createElement("td");
uploadedTd.textContent = file.uploaded;
row.appendChild(uploadedTd);
const sizeTd = document.createElement("td");
sizeTd.textContent = file.size;
row.appendChild(sizeTd);
const uploaderTd = document.createElement("td");
uploaderTd.textContent = file.uploader;
row.appendChild(uploaderTd);
const actionsTd = document.createElement("td");
actionsTd.className = "actions-cell";
const downloadButton = document.createElement("a");
downloadButton.className = "btn btn-sm btn-success";
downloadButton.href = folderPath + encodeURIComponent(file.name);
downloadButton.download = file.name;
downloadButton.textContent = "Download";
actionsTd.appendChild(downloadButton);
if (canEditFile(file.name)) {
const editButton = document.createElement("button");
editButton.className = "btn btn-sm btn-primary ml-2";
editButton.textContent = "Edit";
editButton.addEventListener("click", function () {
editFile(file.name, currentFolder);
});
actionsTd.appendChild(editButton);
}
row.appendChild(actionsTd);
tbody.appendChild(row);
});
table.appendChild(tbody);
fileListContainer.appendChild(table);
updateDeleteSelectedVisibility();
// 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";
}
})
.catch(error => console.error("Error loading file list:", error));
}
function updateDeleteSelectedVisibility() {
const checkboxes = document.querySelectorAll(".file-checkbox");
const deleteBtn = document.getElementById("deleteSelectedBtn");
if (checkboxes.length > 0) {
deleteBtn.style.display = "inline-block";
let anyChecked = false;
checkboxes.forEach(chk => {
if (chk.checked) anyChecked = true;
function renderFileTable(folder) {
const fileListContainer = document.getElementById("fileList");
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);
tableHTML += `<tr>
<td><input type="checkbox" class="file-checkbox" value="${file.name}" onclick="toggleDeleteButton()"></td>
<td>${file.name}</td>
<td style="white-space: nowrap;">${file.modified}</td>
<td style="white-space: nowrap;">${file.uploaded}</td>
<td style="white-space: nowrap;">${file.size}</td>
<td style="white-space: nowrap;">${file.uploader || "Unknown"}</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('${file.name}', '${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);
});
deleteBtn.disabled = !anyChecked;
});
// 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";
} else {
deleteBtn.style.display = "none";
copyBtn.style.display = "none";
moveBtn.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);
}
// Update the visibility and enabled state of the Delete, Copy, and Move buttons
function updateDeleteSelectedVisibility() {
const checkboxes = document.querySelectorAll(".file-checkbox");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const copyBtn = document.getElementById("copySelectedBtn");
const moveBtn = document.getElementById("moveSelectedBtn");
if (checkboxes.length > 0) {
// Show all three action buttons
deleteBtn.style.display = "inline-block";
copyBtn.style.display = "inline-block";
moveBtn.style.display = "inline-block";
let anyChecked = false;
checkboxes.forEach(chk => { if (chk.checked) anyChecked = true; });
deleteBtn.disabled = !anyChecked;
copyBtn.disabled = !anyChecked;
moveBtn.disabled = !anyChecked;
} else {
deleteBtn.style.display = "none";
copyBtn.style.display = "none";
moveBtn.style.display = "none";
}
}
// Delete Selected Files handler (existing)
function handleDeleteSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -539,10 +587,116 @@ document.addEventListener("DOMContentLoaded", function () {
.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
// -----------------------