Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d57687adee | ||
|
|
64d41af21b | ||
|
|
a8f5a6d3bc | ||
|
|
062cfc0dd4 | ||
|
|
32d25b1b69 | ||
|
|
56626aaa40 | ||
|
|
0697fcb1df | ||
|
|
c08c903810 | ||
|
|
2c8374a66c | ||
|
|
49138835ce | ||
|
|
c0dc0ce391 | ||
|
|
6426f4b924 | ||
|
|
b72356b657 | ||
|
|
fc45767712 | ||
|
|
1d5c6a48b5 | ||
|
|
772326c8e0 | ||
|
|
5892236aa9 | ||
|
|
0215bd3d76 |
98
README.md
@@ -1,15 +1,15 @@
|
||||
# MFE - Lightweight Multi File Upload Editor
|
||||
# FileRise - Elevate your File Management
|
||||
|
||||
**Video demo:**
|
||||
|
||||
https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
|
||||
https://github.com/user-attachments/assets/9546a76b-afb0-4068-875a-0eab478b514d
|
||||
|
||||
**Dark mode:**
|
||||

|
||||

|
||||
|
||||
changelogs available here: <https://github.com/error311/multi-file-upload-editor-docker/>
|
||||
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
||||
|
||||
MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, syntax highlight editing, drag & drop and managing files. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive, dynamic file management interface. It serves as an alternative to solutions like FileGator TinyFileManager or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
|
||||
FileRise is a lightweight, secure, self-hosted web application for uploading, syntax-highlight editing, drag & drop file management, and more. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive and dynamic interface designed to simplify file handling. As an alternative to solutions like FileGator, TinyFileManager, or ProjectSend, FileRise provides an easy-to-set-up experience ideal for document management, image galleries, firmware hosting, and other file-intensive applications.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +22,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
||||
- **Real-Time Progress:** Each file shows an individual progress bar that displays percentage complete and upload speed.
|
||||
- **File & Folder Grouping:** When many files are dropped, files are automatically grouped into a scrollable wrapper, ensuring the interface remains clean.
|
||||
- **Secure Uploads:** All uploads integrate CSRF token validation and other security checks.
|
||||
|
||||
- **Built-in File Editing & Renaming:**
|
||||
- Text-based files (e.g., .txt, .html, .js) can be opened and edited in a modal window using CodeMirror for:
|
||||
- Syntax highlighting
|
||||
@@ -30,52 +31,80 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
||||
- Files can be renamed directly through the interface.
|
||||
- The renaming functionality now supports names with parentheses and checks for duplicate names, automatically generating a unique name (e.g., appending “ (1)”) when needed.
|
||||
- Folder-specific metadata is updated accordingly.
|
||||
- **Enhanced File Editing Check:** Files with a Content-Length of 0 KB are now allowed to be edited.
|
||||
|
||||
- **Built-in File Preview:**
|
||||
- Users can quickly preview images, videos, and PDFs directly in modal popups without leaving the page.
|
||||
- The preview modal supports inline display of images (with proper scaling) and videos with playback controls.
|
||||
- Navigation (prev/next) within image previews is supported for a seamless browsing experience.
|
||||
|
||||
- **Gallery (Grid) View:**
|
||||
- In addition to the traditional table view, users can toggle to a gallery view that arranges image thumbnails in a grid layout.
|
||||
- The gallery view offers multiple column options (e.g., 3, 4, or 5 columns) so that users can choose the layout that best fits their screen.
|
||||
- Action buttons (Download, Edit, Rename, Share) appear beneath each thumbnail for quick access.
|
||||
- **Batch Operations (Delete/Copy/Move/Download):**
|
||||
|
||||
- **Batch Operations (Delete/Copy/Move/Download/Extract Zip):**
|
||||
- **Delete Files:** Delete multiple files at once.
|
||||
- **Copy Files:** Copy selected files to another folder with a unique-naming feature to prevent overwrites.
|
||||
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
||||
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
||||
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
||||
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
|
||||
- **Drag & Drop (File Movement):** Easily move files by selecting them from the file list and dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
||||
- **Enhanced Context Menu & Keyboard Shortcuts:**
|
||||
- **Right-Click Context Menu:**
|
||||
- A custom context menu appears on right-clicking within the file list.
|
||||
- For multiple selections, options include Delete Selected, Copy Selected, Move Selected, Download Zip, and (if applicable) Extract Zip.
|
||||
- When exactly one file is selected, additional options (Preview, Edit [if editable], and Rename) are available.
|
||||
- **Keyboard Shortcut for Deletion:**
|
||||
- A global keydown listener detects Delete/Backspace key presses (when no input is focused) to trigger the delete operation.
|
||||
|
||||
- **Folder Management:**
|
||||
- Organize files into folders and subfolders with the ability to create, rename, and delete folders.
|
||||
- A dynamic folder tree in the UI allows users to navigate directories easily, and any changes are immediately reflected in real time.
|
||||
- **Per-Folder Metadata Storage:** Each folder has its own metadata JSON file (e.g., `root_metadata.json`, `FolderName_metadata.json`), and operations (copy/move/rename) update these metadata files accordingly.
|
||||
- **Intuitive Breadcrumb Navigation:** Clickable breadcrumbs enable users to quickly jump to any parent folder, streamlining navigation across subfolders. Supports drag & drop to move files.
|
||||
- **Folder Manager Context Menu:**
|
||||
- Right-clicking on a folder (in the folder tree or breadcrumb) brings up a custom context menu with options for creating, renaming, and deleting folders.
|
||||
- **Keyboard Shortcut for Folder Deletion:**
|
||||
- A global key listener (Delete/Backspace) is provided to trigger folder deletion (with safeguards to prevent deleting the root folder).
|
||||
|
||||
- **Sorting & Pagination:**
|
||||
- The file list can be sorted by name, modified date, upload date, file size, or uploader.
|
||||
- Pagination controls let users navigate through files with selectable page sizes (10, 20, 50, or 100 items per page) and “Prev”/“Next” navigation buttons.
|
||||
|
||||
- **Share Link Functionality:**
|
||||
- Generate shareable links for files with configurable expiration times (e.g., 30, 60, 120, 180, 240 minutes, and a 1-day option) and optional password protection.
|
||||
- Share links are stored in a JSON file with details including the folder, file, expiration timestamp, and hashed password.
|
||||
- The share endpoint (`share.php`) validates tokens, expiration, and password before serving files (or forcing downloads).
|
||||
- The share URL is configurable via environment variables or auto-detected from the server.
|
||||
|
||||
- **User Authentication & Management:**
|
||||
- Secure, session-based authentication protects the file manager.
|
||||
- Admin users can add or remove users through the interface.
|
||||
- Passwords are hashed using PHP’s `password_hash()` for security.
|
||||
- All state-changing endpoints include CSRF token validation.
|
||||
- Change password supported for all users.
|
||||
- **Persistent Login (Remember Me) with Encrypted Tokens:**
|
||||
- Users can remain logged in across sessions securely.
|
||||
- Persistent tokens are encrypted using AES‑256‑CBC before being stored in a JSON file.
|
||||
- On auto-login, the tokens are decrypted on the server to re-establish user sessions without requiring re-authentication.
|
||||
|
||||
- **Responsive, Dynamic & Persistent UI:**
|
||||
- The interface is mobile-friendly and adapts to various screen sizes by hiding non-critical columns on small devices.
|
||||
- Asynchronous updates (via Fetch API and XMLHttpRequest) keep the UI responsive without full page reloads.
|
||||
- Persistent settings (such as items per page, dark/light mode preference, folder tree state, and the last open folder) ensure a smooth and customized user experience.
|
||||
|
||||
- **Dark Mode/Light Mode:**
|
||||
- The application automatically adapts to the operating system’s theme preference by default and offers a manual toggle.
|
||||
- The dark mode provides a darker background with lighter text and adjusts UI elements (including the CodeMirror editor) for optimal readability in low-light conditions.
|
||||
- The light mode maintains a bright interface for well-lit environments.
|
||||
|
||||
- **Server & Security Enhancements:**
|
||||
- The Apache configuration (or .htaccess files) is set to disable directory indexing (e.g., using `Options -Indexes` in the uploads directory), preventing unauthorized users from viewing directory contents.
|
||||
- Direct access to sensitive files (e.g., `users.txt`) is restricted through .htaccess rules.
|
||||
- A proxy download mechanism has been implemented (via endpoints like `download.php` and `downloadZip.php`) so that every file download request goes through a PHP script. This script validates the session and CSRF token before streaming the file, ensuring that even if a file URL is guessed, only authenticated users can access it.
|
||||
- Administrators are advised to deploy the app on a secure internal network or use the proxy download mechanism for public deployments to further protect file content.
|
||||
|
||||
- **Trash Management with Restore & Delete:**
|
||||
- **Trash Storage & Metadata:**
|
||||
- Deleted files are moved to a designated “Trash” folder rather than being immediately removed.
|
||||
@@ -97,29 +126,45 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
||||
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
|
||||
- Material icons with tooltips visually represent the restore and delete actions.
|
||||
|
||||
- **Drag & Drop Cards with Dedicated Drop Zones:**
|
||||
- **Sidebar Drop Zone:**
|
||||
- Cards (such as the upload card or folder management card) can be dragged into a dedicated sidebar drop zone for quick access to frequently used operations.
|
||||
- The sidebar drop zone expands dynamically to accept drops anywhere within its visual area.
|
||||
- **Top Bar Drop Zone:**
|
||||
- A top drop zone is available for reordering or managing cards quickly.
|
||||
- Dragging a card to the top drop zone provides immediate visual feedback, ensuring a fluid and customizable workflow.
|
||||
- **Seamless Interaction:**
|
||||
- Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position.
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
**Light mode:**
|
||||

|
||||

|
||||
|
||||
**Dark mode default:**
|
||||

|
||||
|
||||
**Dark editor:**
|
||||

|
||||

|
||||
|
||||
**Dark preview**
|
||||

|
||||
**Light preview**
|
||||

|
||||
|
||||
**Restore or Delete Trash:**
|
||||

|
||||

|
||||
|
||||
**Login page:**
|
||||

|
||||
**Dark Login page:**
|
||||

|
||||
|
||||
**Gallery view:**
|
||||

|
||||
|
||||
**iphone screenshots:**
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/dark-iphone.png" width="45%">
|
||||
<img src="https://raw.githubusercontent.com/error311/multi-file-upload-editor/refs/heads/master/resources/light-preview-iphone.png" width="45%">
|
||||
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/dark-iphone.png" width="45%">
|
||||
<img src="https://raw.githubusercontent.com/error311/FileRise/refs/heads/master/resources/light-preview-iphone.png" width="45%">
|
||||
</p>
|
||||
|
||||
---
|
||||
@@ -132,7 +177,7 @@ MFE - Multi File Upload Editor is a lightweight, secure, self-hosted web applica
|
||||
- **Clone:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/error311/multi-file-upload-editor.git
|
||||
git clone https://github.com/error311/FileRise.git
|
||||
```
|
||||
|
||||
- **Download:**
|
||||
@@ -182,7 +227,13 @@ For users who prefer containerization, a Docker image is available
|
||||
1. **Pull the Docker Image:**
|
||||
|
||||
```bash
|
||||
docker pull error311/multi-file-upload-editor-docker:latest
|
||||
docker pull error311/filerise-docker:latest
|
||||
```
|
||||
|
||||
macos M series:
|
||||
|
||||
```bash
|
||||
docker pull --platform linux/x86_64 error311/filerise-docker:latest
|
||||
```
|
||||
|
||||
2. **Run the Container:**
|
||||
@@ -196,8 +247,8 @@ For users who prefer containerization, a Docker image is available
|
||||
-v /path/to/your/uploads:/var/www/uploads \
|
||||
-v /path/to/your/users:/var/www/users \
|
||||
-v /path/to/your/metadata:/var/www/metadata \
|
||||
--name multi-file-upload-editor \
|
||||
error311/multi-file-upload-editor-docker:latest
|
||||
--name FileRise \
|
||||
error311/filerise-docker:latest
|
||||
```
|
||||
|
||||
3. **Using Docker Compose:**
|
||||
@@ -208,13 +259,14 @@ For users who prefer containerization, a Docker image is available
|
||||
version: "3.8"
|
||||
services:
|
||||
web:
|
||||
image: error311/multi-file-upload-editor-docker:latest
|
||||
image: error311/filerise-docker:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
TIMEZONE: "America/New_York"
|
||||
TOTAL_UPLOAD_SIZE: "5G"
|
||||
SECURE: "false"
|
||||
PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
|
||||
volumes:
|
||||
- /path/to/your/uploads:/var/www/uploads
|
||||
- /path/to/your/users:/var/www/users
|
||||
@@ -246,7 +298,7 @@ The `config.php` file contains several key constants that may need adjustment fo
|
||||
Defines the maximum upload size (default is `5G`). Ensure that PHP’s `upload_max_filesize` and `post_max_size` in your `php.ini` are consistent with this setting. The startup script (`start.sh`) updates PHP limits at runtime based on this value.
|
||||
|
||||
- **Environment Variables (Docker):**
|
||||
The Docker image supports overriding configuration via environment variables. For example, you can set `SECURE`, `SHARE_URL`, and port settings via the container’s environment.
|
||||
The Docker image supports overriding configuration via environment variables. For example, you can set `SECURE`, `SHARE_URL`, `PERSISTENT_TOKENS_KEY` and port settings via the container’s environment.
|
||||
|
||||
---
|
||||
|
||||
@@ -261,4 +313,4 @@ The `config.php` file contains several key constants that may need adjustment fo
|
||||
- **Logging & Troubleshooting:**
|
||||
Check Apache logs (located in `/var/log/apache2/`) for troubleshooting any issues during deployment or operation.
|
||||
|
||||
Enjoy using the Multi File Upload Editor! For any issues or contributions, please refer to the [GitHub repository](https://github.com/error311/multi-file-upload-editor).
|
||||
Enjoy using the Multi File Upload Editor! For any issues or contributions, please refer to the [GitHub repository](https://github.com/error311/FileRise).
|
||||
|
||||
245
auth.js
@@ -1,75 +1,112 @@
|
||||
import { sendRequest } from './networkUtils.js';
|
||||
import { toggleVisibility, showToast } from './domUtils.js';
|
||||
import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmModal } from './domUtils.js';
|
||||
import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
|
||||
function initAuth() {
|
||||
// First, check if the user is already authenticated.
|
||||
checkAuthentication(false).then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
return;
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
// User is logged in—show the main UI.
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
// If admin, show admin-only buttons.
|
||||
if (data.isAdmin) {
|
||||
const addUserBtn = document.getElementById("addUserBtn");
|
||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||
if (addUserBtn) addUserBtn.style.display = "block";
|
||||
if (removeUserBtn) removeUserBtn.style.display = "block";
|
||||
// Create and show the restore button.
|
||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (!restoreBtn) {
|
||||
restoreBtn = document.createElement("button");
|
||||
restoreBtn.id = "restoreFilesBtn";
|
||||
restoreBtn.classList.add("btn", "btn-warning");
|
||||
// Use a material icon.
|
||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
if (headerButtons) {
|
||||
if (headerButtons.children.length >= 5) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
|
||||
} else {
|
||||
headerButtons.appendChild(restoreBtn);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates the select element to reflect the stored items-per-page value.
|
||||
*/
|
||||
function updateItemsPerPageSelect() {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||
selectElem.value = stored;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI for an authenticated user.
|
||||
* This includes showing the main UI panels, attaching key listeners, updating header buttons,
|
||||
* and displaying admin-only buttons if applicable.
|
||||
*/
|
||||
function updateAuthenticatedUI(data) {
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", true);
|
||||
toggleVisibility("uploadFileForm", true);
|
||||
toggleVisibility("fileListContainer", true);
|
||||
attachEnterKeyListener("addUserModal", "saveUserBtn");
|
||||
attachEnterKeyListener("removeUserModal", "deleteUserBtn");
|
||||
attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn");
|
||||
document.querySelector(".header-buttons").style.visibility = "visible";
|
||||
|
||||
// If admin, show admin-only buttons; otherwise hide them.
|
||||
if (data.isAdmin) {
|
||||
const addUserBtn = document.getElementById("addUserBtn");
|
||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||
if (addUserBtn) addUserBtn.style.display = "block";
|
||||
if (removeUserBtn) removeUserBtn.style.display = "block";
|
||||
let restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (!restoreBtn) {
|
||||
restoreBtn = document.createElement("button");
|
||||
restoreBtn.id = "restoreFilesBtn";
|
||||
restoreBtn.classList.add("btn", "btn-warning");
|
||||
// Using a material icon for restore.
|
||||
restoreBtn.innerHTML = '<i class="material-icons" title="Restore/Delete Trash">restore_from_trash</i>';
|
||||
const headerButtons = document.querySelector(".header-buttons");
|
||||
if (headerButtons) {
|
||||
if (headerButtons.children.length >= 5) {
|
||||
headerButtons.insertBefore(restoreBtn, headerButtons.children[5]);
|
||||
} else {
|
||||
headerButtons.appendChild(restoreBtn);
|
||||
}
|
||||
restoreBtn.style.display = "block";
|
||||
}
|
||||
}
|
||||
restoreBtn.style.display = "block";
|
||||
} else {
|
||||
const addUserBtn = document.getElementById("addUserBtn");
|
||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||
if (addUserBtn) addUserBtn.style.display = "none";
|
||||
if (removeUserBtn) removeUserBtn.style.display = "none";
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) restoreBtn.style.display = "none";
|
||||
}
|
||||
updateItemsPerPageSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the user's authentication state and updates the UI accordingly.
|
||||
* If in setup mode or not authenticated, it shows the proper UI elements.
|
||||
* When authenticated, it calls updateAuthenticatedUI to handle the UI updates.
|
||||
*/
|
||||
function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById('newUsername').focus();
|
||||
return false;
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
updateAuthenticatedUI(data);
|
||||
return data;
|
||||
} else {
|
||||
const addUserBtn = document.getElementById("addUserBtn");
|
||||
const removeUserBtn = document.getElementById("removeUserBtn");
|
||||
if (addUserBtn) addUserBtn.style.display = "none";
|
||||
if (removeUserBtn) removeUserBtn.style.display = "none";
|
||||
const restoreBtn = document.getElementById("restoreFilesBtn");
|
||||
if (restoreBtn) {
|
||||
restoreBtn.style.display = "none";
|
||||
}
|
||||
if (showLoginToast) showToast("Please log in to continue.");
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
return false;
|
||||
}
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||
selectElem.value = stored;
|
||||
}
|
||||
} else {
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
}
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error checking authentication:", error);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes authentication by checking the user's state and setting up event listeners.
|
||||
* The UI will update automatically based on the auth state.
|
||||
*/
|
||||
function initAuth() {
|
||||
checkAuthentication(false).catch(error => {
|
||||
console.error("Error checking authentication:", error);
|
||||
});
|
||||
|
||||
@@ -78,9 +115,8 @@ function initAuth() {
|
||||
if (authForm) {
|
||||
authForm.addEventListener("submit", function (event) {
|
||||
event.preventDefault();
|
||||
// Get the "Remember me" checkbox value.
|
||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||
? document.getElementById("rememberMeCheckbox").checked
|
||||
const rememberMe = document.getElementById("rememberMeCheckbox")
|
||||
? document.getElementById("rememberMeCheckbox").checked
|
||||
: false;
|
||||
const formData = {
|
||||
username: document.getElementById("loginUsername").value.trim(),
|
||||
@@ -128,13 +164,12 @@ function initAuth() {
|
||||
document.getElementById("addUserBtn").addEventListener("click", function () {
|
||||
resetUserForm();
|
||||
toggleVisibility("addUserModal", true);
|
||||
document.getElementById('newUsername').focus();
|
||||
});
|
||||
document.getElementById("saveUserBtn").addEventListener("click", function () {
|
||||
const newUsername = document.getElementById("newUsername").value.trim();
|
||||
// Use the new ID for the add user modal's password field.
|
||||
const newPassword = document.getElementById("addUserPassword").value.trim();
|
||||
const isAdmin = document.getElementById("isAdmin").checked;
|
||||
console.log("newUsername:", newUsername, "newPassword:", newPassword);
|
||||
if (!newUsername || !newPassword) {
|
||||
showToast("Username and password are required!");
|
||||
return;
|
||||
@@ -157,6 +192,7 @@ function initAuth() {
|
||||
if (data.success) {
|
||||
showToast("User added successfully!");
|
||||
closeAddUserModal();
|
||||
// Re-check auth state to update the UI after adding a user.
|
||||
checkAuthentication(false);
|
||||
} else {
|
||||
showToast("Error: " + (data.error || "Could not add user"));
|
||||
@@ -173,14 +209,16 @@ function initAuth() {
|
||||
loadUserList();
|
||||
toggleVisibility("removeUserModal", true);
|
||||
});
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", function () {
|
||||
|
||||
document.getElementById("deleteUserBtn").addEventListener("click", async function () {
|
||||
const selectElem = document.getElementById("removeUsernameSelect");
|
||||
const usernameToRemove = selectElem.value;
|
||||
if (!usernameToRemove) {
|
||||
showToast("Please select a user to remove.");
|
||||
return;
|
||||
}
|
||||
if (!confirm("Are you sure you want to delete user " + usernameToRemove + "?")) {
|
||||
const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?");
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
fetch("removeUser.php", {
|
||||
@@ -204,39 +242,33 @@ function initAuth() {
|
||||
})
|
||||
.catch(error => console.error("Error removing user:", error));
|
||||
});
|
||||
|
||||
document.getElementById("cancelRemoveUserBtn").addEventListener("click", function () {
|
||||
closeRemoveUserModal();
|
||||
});
|
||||
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function() {
|
||||
// Show the Change Password modal.
|
||||
document.getElementById("changePasswordBtn").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "block";
|
||||
document.getElementById("oldPassword").focus();
|
||||
});
|
||||
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function() {
|
||||
// Hide the Change Password modal.
|
||||
|
||||
document.getElementById("closeChangePasswordModal").addEventListener("click", function () {
|
||||
document.getElementById("changePasswordModal").style.display = "none";
|
||||
});
|
||||
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function() {
|
||||
|
||||
document.getElementById("saveNewPasswordBtn").addEventListener("click", function () {
|
||||
const oldPassword = document.getElementById("oldPassword").value.trim();
|
||||
const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field
|
||||
const newPassword = document.getElementById("newPassword").value.trim();
|
||||
const confirmPassword = document.getElementById("confirmPassword").value.trim();
|
||||
|
||||
if (!oldPassword || !newPassword || !confirmPassword) {
|
||||
showToast("Please fill in all fields.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast("New passwords do not match.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the data to send.
|
||||
const data = { oldPassword, newPassword, confirmPassword };
|
||||
|
||||
// Send request to changePassword.php.
|
||||
fetch("changePassword.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
@@ -250,7 +282,6 @@ function initAuth() {
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast(result.success);
|
||||
// Clear form fields and close modal.
|
||||
document.getElementById("oldPassword").value = "";
|
||||
document.getElementById("newPassword").value = "";
|
||||
document.getElementById("confirmPassword").value = "";
|
||||
@@ -266,38 +297,6 @@ function initAuth() {
|
||||
});
|
||||
}
|
||||
|
||||
function checkAuthentication(showLoginToast = true) {
|
||||
return sendRequest("checkAuth.php")
|
||||
.then(data => {
|
||||
if (data.setup) {
|
||||
window.setupMode = true;
|
||||
if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user.");
|
||||
toggleVisibility("loginForm", false);
|
||||
toggleVisibility("mainOperations", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
toggleVisibility("addUserModal", true);
|
||||
return false;
|
||||
}
|
||||
window.setupMode = false;
|
||||
if (data.authenticated) {
|
||||
return data;
|
||||
} else {
|
||||
if (showLoginToast) showToast("Please log in to continue.");
|
||||
toggleVisibility("loginForm", true);
|
||||
toggleVisibility("mainOperations", false);
|
||||
toggleVisibility("uploadFileForm", false);
|
||||
toggleVisibility("fileListContainer", false);
|
||||
document.querySelector(".header-buttons").style.visibility = "hidden";
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error checking authentication:", error);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
window.checkAuthentication = checkAuthentication;
|
||||
|
||||
window.changeItemsPerPage = function (value) {
|
||||
localStorage.setItem("itemsPerPage", value);
|
||||
const folder = window.currentFolder || "root";
|
||||
@@ -307,16 +306,12 @@ window.changeItemsPerPage = function (value) {
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const selectElem = document.querySelector(".form-control.bottom-select");
|
||||
if (selectElem) {
|
||||
const stored = localStorage.getItem("itemsPerPage") || "10";
|
||||
selectElem.value = stored;
|
||||
}
|
||||
updateItemsPerPageSelect();
|
||||
});
|
||||
|
||||
function resetUserForm() {
|
||||
document.getElementById("newUsername").value = "";
|
||||
document.getElementById("addUserPassword").value = ""; // Updated for add user modal
|
||||
document.getElementById("addUserPassword").value = "";
|
||||
}
|
||||
|
||||
function closeAddUserModal() {
|
||||
|
||||
13
auth.php
@@ -99,20 +99,25 @@ if ($userRole !== false) {
|
||||
// Generate a secure random token.
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiry = time() + (30 * 24 * 60 * 60); // 30 days
|
||||
|
||||
// Load existing persistent tokens.
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (!is_array($persistentTokens)) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
// Save token along with username and expiry.
|
||||
// Save token along with username, expiry, and admin status.
|
||||
$persistentTokens[$token] = [
|
||||
"username" => $username,
|
||||
"expiry" => $expiry
|
||||
"expiry" => $expiry,
|
||||
"isAdmin" => ($userRole === "1")
|
||||
];
|
||||
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
|
||||
$encryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $encryptedContent, LOCK_EX);
|
||||
// Set the cookie. (Assuming $secure is defined in config.php.)
|
||||
setcookie('remember_me_token', $token, $expiry, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
99
config.php
@@ -1,6 +1,57 @@
|
||||
<?php
|
||||
// config.php
|
||||
|
||||
// Define constants first.
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
define('META_DIR', '/var/www/metadata/');
|
||||
define('META_FILE', 'file_metadata.json');
|
||||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
|
||||
// Set the default timezone.
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
|
||||
/**
|
||||
* Encrypts data using AES-256-CBC.
|
||||
*
|
||||
* @param string $data The plaintext data.
|
||||
* @param string $encryptionKey The secret encryption key.
|
||||
* @return string Base64-encoded string containing IV and ciphertext.
|
||||
*/
|
||||
function encryptData($data, $encryptionKey) {
|
||||
$cipher = 'AES-256-CBC';
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = openssl_random_pseudo_bytes($ivlen);
|
||||
$ciphertext = openssl_encrypt($data, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
return base64_encode($iv . $ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts data encrypted with AES-256-CBC.
|
||||
*
|
||||
* @param string $encryptedData The Base64-encoded data containing IV and ciphertext.
|
||||
* @param string $encryptionKey The secret encryption key.
|
||||
* @return string|false The decrypted plaintext or false on failure.
|
||||
*/
|
||||
function decryptData($encryptedData, $encryptionKey) {
|
||||
$cipher = 'AES-256-CBC';
|
||||
$data = base64_decode($encryptedData);
|
||||
$ivlen = openssl_cipher_iv_length($cipher);
|
||||
$iv = substr($data, 0, $ivlen);
|
||||
$ciphertext = substr($data, $ivlen);
|
||||
return openssl_decrypt($ciphertext, $cipher, $encryptionKey, OPENSSL_RAW_DATA, $iv);
|
||||
}
|
||||
|
||||
// Load encryption key from an environment variable (default for testing; override in production)
|
||||
$encryptionKey = getenv('PERSISTENT_TOKENS_KEY') ?: 'default_please_change_this_key';
|
||||
if (!$encryptionKey) {
|
||||
die('Encryption key for persistent tokens is not set.');
|
||||
}
|
||||
|
||||
// Allow an environment variable to override HTTPS detection.
|
||||
$envSecure = getenv('SECURE');
|
||||
if ($envSecure !== false) {
|
||||
@@ -23,6 +74,7 @@ session_set_cookie_params($cookieParams);
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
session_start();
|
||||
|
||||
// Generate CSRF token if not already set.
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
@@ -30,22 +82,28 @@ if (empty($_SESSION['csrf_token'])) {
|
||||
// Auto-login via persistent token if session is not active.
|
||||
if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token'])) {
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
$persistentTokens = [];
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$persistentTokens = json_decode(file_get_contents($persistentTokensFile), true);
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
// Token is valid; auto-authenticate the user.
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
// Optionally, set admin status if stored in token data:
|
||||
// $_SESSION["isAdmin"] = $tokenData["isAdmin"];
|
||||
} else {
|
||||
// Token expired; remove it and clear the cookie.
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
file_put_contents($persistentTokensFile, json_encode($persistentTokens, JSON_PRETTY_PRINT));
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (!is_array($persistentTokens)) {
|
||||
$persistentTokens = [];
|
||||
}
|
||||
}
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$_COOKIE['remember_me_token']])) {
|
||||
$tokenData = $persistentTokens[$_COOKIE['remember_me_token']];
|
||||
if ($tokenData['expiry'] >= time()) {
|
||||
// Token is valid; auto-authenticate the user.
|
||||
$_SESSION["authenticated"] = true;
|
||||
$_SESSION["username"] = $tokenData["username"];
|
||||
$_SESSION["isAdmin"] = $tokenData["isAdmin"]; // Restore admin status from the token
|
||||
} else {
|
||||
// Token expired; remove it and clear the cookie.
|
||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,15 +122,4 @@ if (strpos(BASE_URL, 'yourwebsite') !== false) {
|
||||
}
|
||||
|
||||
define('SHARE_URL', getenv('SHARE_URL') ? getenv('SHARE_URL') : $defaultShareUrl);
|
||||
|
||||
define('UPLOAD_DIR', '/var/www/uploads/');
|
||||
define('TIMEZONE', 'America/New_York');
|
||||
define('DATE_TIME_FORMAT', 'm/d/y h:iA');
|
||||
define('TOTAL_UPLOAD_SIZE', '5G');
|
||||
define('USERS_DIR', '/var/www/users/');
|
||||
define('USERS_FILE', 'users.txt');
|
||||
define('META_DIR','/var/www/metadata/');
|
||||
define('META_FILE','file_metadata.json');
|
||||
define('TRASH_DIR', UPLOAD_DIR . 'trash/');
|
||||
date_default_timezone_set(TIMEZONE);
|
||||
?>
|
||||
75
domUtils.js
@@ -28,35 +28,39 @@ export function toggleAllCheckboxes(masterCheckbox) {
|
||||
}
|
||||
|
||||
export function updateFileActionButtons() {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox");
|
||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||
const copyBtn = document.getElementById("copySelectedBtn");
|
||||
const moveBtn = document.getElementById("moveSelectedBtn");
|
||||
const deleteBtn = document.getElementById("deleteSelectedBtn");
|
||||
const zipBtn = document.getElementById("downloadZipBtn");
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
|
||||
if (fileCheckboxes.length === 0) {
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (moveBtn) moveBtn.style.display = "none";
|
||||
if (deleteBtn) deleteBtn.style.display = "none";
|
||||
if (zipBtn) zipBtn.style.display = "none";
|
||||
if (extractZipBtn) extractZipBtn.style.display = "none";
|
||||
} else {
|
||||
if (copyBtn) copyBtn.style.display = "inline-block";
|
||||
if (moveBtn) moveBtn.style.display = "inline-block";
|
||||
if (deleteBtn) deleteBtn.style.display = "inline-block";
|
||||
if (zipBtn) zipBtn.style.display = "inline-block";
|
||||
if (extractZipBtn) extractZipBtn.style.display = "inline-block";
|
||||
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
if (copyBtn) copyBtn.disabled = false;
|
||||
if (moveBtn) moveBtn.disabled = false;
|
||||
if (deleteBtn) deleteBtn.disabled = false;
|
||||
if (zipBtn) zipBtn.disabled = false;
|
||||
} else {
|
||||
if (copyBtn) copyBtn.disabled = true;
|
||||
if (moveBtn) moveBtn.disabled = true;
|
||||
if (deleteBtn) deleteBtn.disabled = true;
|
||||
if (zipBtn) zipBtn.disabled = true;
|
||||
const anySelected = selectedCheckboxes.length > 0;
|
||||
if (copyBtn) copyBtn.disabled = !anySelected;
|
||||
if (moveBtn) moveBtn.disabled = !anySelected;
|
||||
if (deleteBtn) deleteBtn.disabled = !anySelected;
|
||||
if (zipBtn) zipBtn.disabled = !anySelected;
|
||||
|
||||
if (extractZipBtn) {
|
||||
// Enable only if at least one selected file ends with .zip (case-insensitive).
|
||||
const anyZipSelected = Array.from(selectedCheckboxes).some(chk =>
|
||||
chk.value.toLowerCase().endsWith(".zip")
|
||||
);
|
||||
extractZipBtn.disabled = !anyZipSelected;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,4 +309,53 @@ export function previewFile(fileUrl, fileName) {
|
||||
}
|
||||
|
||||
modal.style.display = "flex";
|
||||
}
|
||||
|
||||
export function attachEnterKeyListener(modalId, buttonId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
// Make the modal focusable
|
||||
modal.setAttribute("tabindex", "-1");
|
||||
modal.focus();
|
||||
modal.addEventListener("keydown", function(e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById(buttonId);
|
||||
if (btn) {
|
||||
btn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showCustomConfirmModal(message) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
messageElem.textContent = message;
|
||||
modal.style.display = "block";
|
||||
|
||||
// Cleanup function to hide the modal and remove event listeners.
|
||||
function cleanup() {
|
||||
modal.style.display = "none";
|
||||
yesBtn.removeEventListener("click", onYes);
|
||||
noBtn.removeEventListener("click", onNo);
|
||||
}
|
||||
|
||||
function onYes() {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
}
|
||||
function onNo() {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
yesBtn.addEventListener("click", onYes);
|
||||
noBtn.addEventListener("click", onNo);
|
||||
});
|
||||
}
|
||||
364
dragAndDrop.js
Normal file
@@ -0,0 +1,364 @@
|
||||
// dragAndDrop.js
|
||||
|
||||
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||
export function loadSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
// Ensure main wrapper is visible.
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) {
|
||||
mainWrapper.style.display = 'flex';
|
||||
}
|
||||
// For each saved ID, move the card into the sidebar.
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
// Animate vertical slide for sidebar card
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
// Internal helper: update sidebar visibility based on its content.
|
||||
function updateSidebarVisibility() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
if (cards.length > 0) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
} else {
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
// Save the current order in localStorage.
|
||||
saveSidebarOrder();
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||
function updateTopZoneLayout() {
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
||||
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
||||
|
||||
if (leftIsEmpty && !rightIsEmpty) {
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
} else {
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
leftCol.style.margin = '';
|
||||
rightCol.style.margin = '';
|
||||
}
|
||||
}
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
function addTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.add('highlight');
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
topZone.style.minHeight = '375px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the drag ends, remove the extra min-height.
|
||||
function removeTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical slide/fade animation helper.
|
||||
function animateVerticalSlide(card) {
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
// Force reflow.
|
||||
card.offsetWidth;
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.opacity = '1';
|
||||
});
|
||||
setTimeout(() => {
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
}, 310);
|
||||
}
|
||||
|
||||
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||
function insertCardInSidebar(card, event) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
let inserted = false;
|
||||
for (const currentCard of existingCards) {
|
||||
const rect = currentCard.getBoundingClientRect();
|
||||
const midY = rect.top + rect.height / 2;
|
||||
if (event.clientY < midY) {
|
||||
sidebar.insertBefore(card, currentCard);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
sidebar.appendChild(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
card.style.width = '100%';
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
|
||||
// Internal helper: save the current sidebar card order to localStorage.
|
||||
function saveSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
const order = Array.from(cards).map(card => card.id);
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||
function moveSidebarCardsToTop() {
|
||||
if (window.innerWidth < 1205) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
cards.forEach(card => {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
updateSidebarVisibility();
|
||||
updateTopZoneLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.innerWidth < 1205) {
|
||||
moveSidebarCardsToTop();
|
||||
}
|
||||
});
|
||||
|
||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||
function ensureTopZonePlaceholder() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (!topZone) return;
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
let placeholder = topZone.querySelector('.placeholder');
|
||||
if (!placeholder) {
|
||||
placeholder = document.createElement('div');
|
||||
placeholder.className = 'placeholder';
|
||||
placeholder.style.visibility = 'hidden';
|
||||
placeholder.style.display = 'block';
|
||||
placeholder.style.width = '100%';
|
||||
placeholder.style.height = '375px';
|
||||
topZone.appendChild(placeholder);
|
||||
}
|
||||
} else {
|
||||
const placeholder = topZone.querySelector('.placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// This sets up all drag-and-drop event listeners for cards.
|
||||
export function initDragAndDrop() {
|
||||
function run() {
|
||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
draggableCards.forEach(card => {
|
||||
if (!card.dataset.originalContainerId) {
|
||||
card.dataset.originalContainerId = card.parentNode.id;
|
||||
}
|
||||
const header = card.querySelector('.card-header');
|
||||
if (header) {
|
||||
header.classList.add('drag-header');
|
||||
}
|
||||
|
||||
let isDragging = false;
|
||||
let dragTimer = null;
|
||||
let offsetX = 0, offsetY = 0;
|
||||
let initialLeft, initialTop;
|
||||
|
||||
if (header) {
|
||||
header.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
const card = this.closest('.card');
|
||||
const rect = card.getBoundingClientRect();
|
||||
const originX = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const originY = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
dragTimer = setTimeout(() => {
|
||||
isDragging = true;
|
||||
card.classList.add('dragging');
|
||||
// Disable pointer events on the card so it doesn't block drop detection.
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
// Force the sidebar to have a tall drop zone while dragging.
|
||||
sidebar.style.height = '800px';
|
||||
}
|
||||
const rect = card.getBoundingClientRect();
|
||||
initialLeft = rect.left + window.pageXOffset;
|
||||
initialTop = rect.top + window.pageYOffset;
|
||||
offsetX = e.pageX - initialLeft;
|
||||
offsetY = e.pageY - initialTop;
|
||||
document.body.appendChild(card);
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = initialLeft + 'px';
|
||||
card.style.top = initialTop + 'px';
|
||||
card.style.width = rect.width + 'px';
|
||||
card.style.zIndex = '10000';
|
||||
}, 500);
|
||||
});
|
||||
header.addEventListener('mouseup', function () {
|
||||
clearTimeout(dragTimer);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (isDragging) {
|
||||
card.style.left = (e.pageX - offsetX) + 'px';
|
||||
card.style.top = (e.pageY - offsetY) + 'px';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function (e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
// Re-enable pointer events on the card.
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
// Remove the forced height once the drag ends.
|
||||
sidebar.style.height = '';
|
||||
}
|
||||
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
let droppedInSidebar = false;
|
||||
let droppedInTop = false;
|
||||
|
||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||
if (sidebarElem) {
|
||||
// Instead of using elementsFromPoint, use a virtual drop zone.
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
// Define a drop zone from the top of the sidebar to 1000px below its top.
|
||||
const dropZoneBottom = rect.top + 800;
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
sidebarElem.blur();
|
||||
}
|
||||
}
|
||||
|
||||
const topRow = document.getElementById('uploadFolderRow');
|
||||
if (!droppedInSidebar && topRow) {
|
||||
const rect = topRow.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
let container;
|
||||
if (card.id === 'uploadCard') {
|
||||
container = document.getElementById('leftCol');
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
container = document.getElementById('rightCol');
|
||||
}
|
||||
if (container) {
|
||||
ensureTopZonePlaceholder();
|
||||
container.appendChild(card);
|
||||
droppedInTop = true;
|
||||
|
||||
container.style.position = 'relative';
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = '0px';
|
||||
|
||||
// Animate vertical slide/fade.
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
|
||||
card.offsetWidth; // Force reflow.
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.opacity = '1';
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
card.style.position = '';
|
||||
container.style.position = '';
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
card.style.width = '';
|
||||
}, 310);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (droppedInSidebar || droppedInTop) {
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
} else {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
card.style.width = '';
|
||||
}
|
||||
}
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
155
extractZip.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
require_once 'config.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// --- CSRF Protection ---
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
if ($receivedToken !== $_SESSION['csrf_token']) {
|
||||
http_response_code(403);
|
||||
echo json_encode(["error" => "Invalid CSRF token"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Ensure user is authenticated.
|
||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(["error" => "Unauthorized"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read and decode the JSON input.
|
||||
$rawData = file_get_contents("php://input");
|
||||
$data = json_decode($rawData, true);
|
||||
if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid input."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folder = $data['folder'];
|
||||
$files = $data['files'];
|
||||
|
||||
if (empty($files)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "No files specified."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate folder name (allow "root" or valid subfolder names).
|
||||
if ($folder !== "root") {
|
||||
$parts = explode('/', $folder);
|
||||
foreach ($parts as $part) {
|
||||
if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(["error" => "Invalid folder name."]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR;
|
||||
} else {
|
||||
$relativePath = "";
|
||||
}
|
||||
|
||||
$baseDir = realpath(UPLOAD_DIR);
|
||||
if ($baseDir === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(["error" => "Uploads directory not configured correctly."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath;
|
||||
$folderPathReal = realpath($folderPath);
|
||||
if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) {
|
||||
http_response_code(404);
|
||||
echo json_encode(["error" => "Folder not found."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ---------- Metadata Setup ----------
|
||||
function getMetadataFilePath($folder) {
|
||||
if (strtolower($folder) === 'root' || $folder === '') {
|
||||
return META_DIR . "root_metadata.json";
|
||||
}
|
||||
return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json';
|
||||
}
|
||||
|
||||
$srcMetaFile = getMetadataFilePath($folder);
|
||||
$destMetaFile = getMetadataFilePath($folder);
|
||||
$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : [];
|
||||
$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : [];
|
||||
|
||||
$errors = [];
|
||||
$allSuccess = true;
|
||||
$extractedFiles = array(); // Array to collect names of extracted files
|
||||
$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/';
|
||||
|
||||
// ---------- Process Each File ----------
|
||||
foreach ($files as $zipFileName) {
|
||||
$originalName = basename(trim($zipFileName));
|
||||
// Process only .zip files.
|
||||
if (strtolower(substr($originalName, -4)) !== '.zip') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match($safeFileNamePattern, $originalName)) {
|
||||
$errors[] = "$originalName has an invalid name.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName;
|
||||
if (!file_exists($zipFilePath)) {
|
||||
$errors[] = "$originalName does not exist in folder.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($zipFilePath) !== TRUE) {
|
||||
$errors[] = "Could not open $originalName as a zip file.";
|
||||
$allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt extraction.
|
||||
if (!$zip->extractTo($folderPathReal)) {
|
||||
$errors[] = "Failed to extract $originalName.";
|
||||
$allSuccess = false;
|
||||
} else {
|
||||
// Collect extracted file names from this zip.
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = $zip->getNameIndex($i);
|
||||
$extractedFileName = basename($entryName);
|
||||
if ($extractedFileName) {
|
||||
$extractedFiles[] = $extractedFileName;
|
||||
}
|
||||
}
|
||||
// Update metadata for each extracted file if the zip file has metadata.
|
||||
if (isset($srcMetadata[$originalName])) {
|
||||
$zipMeta = $srcMetadata[$originalName];
|
||||
// Iterate through all entries in the zip.
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$entryName = $zip->getNameIndex($i);
|
||||
$extractedFileName = basename($entryName);
|
||||
if ($extractedFileName) {
|
||||
$destMetadata[$extractedFileName] = $zipMeta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
// Write updated metadata back to the destination metadata file.
|
||||
if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) {
|
||||
$errors[] = "Failed to update metadata.";
|
||||
$allSuccess = false;
|
||||
}
|
||||
|
||||
if ($allSuccess) {
|
||||
echo json_encode(["success" => true, "extractedFiles" => $extractedFiles]);
|
||||
} else {
|
||||
echo json_encode(["success" => false, "error" => implode(" ", $errors)]);
|
||||
}
|
||||
exit;
|
||||
?>
|
||||
260
fileManager.js
@@ -9,6 +9,7 @@ import {
|
||||
showToast,
|
||||
updateRowHighlight,
|
||||
toggleRowSelection,
|
||||
attachEnterKeyListener,
|
||||
previewFile as originalPreviewFile
|
||||
} from './domUtils.js';
|
||||
|
||||
@@ -661,6 +662,7 @@ export function handleDeleteSelected(e) {
|
||||
document.getElementById("deleteFilesMessage").textContent =
|
||||
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
|
||||
document.getElementById("deleteFilesModal").style.display = "block";
|
||||
attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles");
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -671,6 +673,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
window.filesToDelete = [];
|
||||
});
|
||||
}
|
||||
|
||||
const confirmDelete = document.getElementById("confirmDeleteFiles");
|
||||
if (confirmDelete) {
|
||||
confirmDelete.addEventListener("click", function () {
|
||||
@@ -700,7 +703,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
attachEnterKeyListener("downloadZipModal", "confirmDownloadZip");
|
||||
export function handleDownloadZipSelected(e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
@@ -711,6 +714,69 @@ export function handleDownloadZipSelected(e) {
|
||||
}
|
||||
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
|
||||
document.getElementById("downloadZipModal").style.display = "block";
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("zipFileNameInput");
|
||||
input.focus();
|
||||
}, 100);
|
||||
|
||||
}
|
||||
|
||||
export function handleExtractZipSelected(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
// Get selected file names
|
||||
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
|
||||
if (!checkboxes.length) {
|
||||
showToast("No files selected.");
|
||||
return;
|
||||
}
|
||||
// Filter for zip files only
|
||||
const zipFiles = Array.from(checkboxes)
|
||||
.map(chk => chk.value)
|
||||
.filter(name => name.toLowerCase().endsWith(".zip"));
|
||||
if (!zipFiles.length) {
|
||||
showToast("No zip files selected.");
|
||||
return;
|
||||
}
|
||||
// Call the extract endpoint with the selected zip files
|
||||
fetch("extractZip.php", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
folder: window.currentFolder || "root",
|
||||
files: zipFiles
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// If the server returned a list of extracted files, join them into a string.
|
||||
let toastMessage = "Zip file(s) extracted successfully!";
|
||||
if (data.extractedFiles && Array.isArray(data.extractedFiles) && data.extractedFiles.length) {
|
||||
toastMessage = "Extracted: " + data.extractedFiles.join(", ");
|
||||
}
|
||||
showToast(toastMessage);
|
||||
loadFileList(window.currentFolder);
|
||||
} else {
|
||||
showToast("Error extracting zip: " + (data.error || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error extracting zip files:", error);
|
||||
showToast("Error extracting zip files.");
|
||||
});
|
||||
}
|
||||
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
if (extractZipBtn) {
|
||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
@@ -720,6 +786,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
document.getElementById("downloadZipModal").style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
|
||||
if (confirmDownloadZip) {
|
||||
confirmDownloadZip.addEventListener("click", function () {
|
||||
@@ -1035,7 +1102,7 @@ export function editFile(fileName, folder) {
|
||||
fetch(fileUrl, { method: "HEAD" })
|
||||
.then(response => {
|
||||
const contentLength = response.headers.get("Content-Length");
|
||||
if (!contentLength || parseInt(contentLength) > 10485760) {
|
||||
if (contentLength !== null && parseInt(contentLength) > 10485760) {
|
||||
showToast("This file is larger than 10 MB and cannot be edited in the browser.");
|
||||
throw new Error("File too large.");
|
||||
}
|
||||
@@ -1196,13 +1263,28 @@ export function initFileActions() {
|
||||
downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true));
|
||||
document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected);
|
||||
}
|
||||
const extractZipBtn = document.getElementById("extractZipBtn");
|
||||
if (extractZipBtn) {
|
||||
extractZipBtn.replaceWith(extractZipBtn.cloneNode(true));
|
||||
document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected);
|
||||
}
|
||||
}
|
||||
|
||||
attachEnterKeyListener("renameFileModal", "submitRenameFile");
|
||||
export function renameFile(oldName, folder) {
|
||||
window.fileToRename = oldName;
|
||||
window.fileFolder = folder || window.currentFolder || "root";
|
||||
document.getElementById("newFileName").value = oldName;
|
||||
document.getElementById("renameFileModal").style.display = "block";
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("newFileName");
|
||||
input.focus();
|
||||
const lastDot = oldName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
input.setSelectionRange(0, lastDot);
|
||||
} else {
|
||||
input.select();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@@ -1213,6 +1295,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("newFileName").value = "";
|
||||
});
|
||||
}
|
||||
|
||||
const submitBtn = document.getElementById("submitRenameFile");
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener("click", function () {
|
||||
@@ -1273,4 +1356,173 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
el.addEventListener("dragleave", folderDragLeaveHandler);
|
||||
el.addEventListener("drop", folderDropHandler);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function(e) {
|
||||
// Skip if focus is on an input, textarea, or any contentEditable element.
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
// On Mac, the delete key is often reported as "Backspace" (keyCode 8)
|
||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||
if (selectedCheckboxes.length > 0) {
|
||||
e.preventDefault(); // Prevent default back navigation in some browsers.
|
||||
handleDeleteSelected(new Event("click"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ----------
|
||||
|
||||
// Function to display the context menu with provided items at (x, y)
|
||||
// Function to display the context menu with provided items at (x, y)
|
||||
function showFileContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("fileContextMenu");
|
||||
if (!menu) {
|
||||
menu = document.createElement("div");
|
||||
menu.id = "fileContextMenu";
|
||||
// Use fixed positioning so the menu is relative to the viewport
|
||||
menu.style.position = "fixed";
|
||||
menu.style.backgroundColor = "#fff";
|
||||
menu.style.border = "1px solid #ccc";
|
||||
menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)";
|
||||
menu.style.zIndex = "9999";
|
||||
menu.style.padding = "5px 0";
|
||||
menu.style.minWidth = "150px";
|
||||
document.body.appendChild(menu);
|
||||
}
|
||||
// Clear previous items
|
||||
menu.innerHTML = "";
|
||||
menuItems.forEach(item => {
|
||||
let menuItem = document.createElement("div");
|
||||
menuItem.textContent = item.label;
|
||||
menuItem.style.padding = "5px 15px";
|
||||
menuItem.style.cursor = "pointer";
|
||||
menuItem.addEventListener("mouseover", () => {
|
||||
menuItem.style.backgroundColor = document.body.classList.contains("dark-mode") ? "#444" : "#f0f0f0";
|
||||
});
|
||||
menuItem.addEventListener("mouseout", () => {
|
||||
menuItem.style.backgroundColor = "";
|
||||
});
|
||||
menuItem.addEventListener("click", () => {
|
||||
item.action();
|
||||
hideFileContextMenu();
|
||||
});
|
||||
menu.appendChild(menuItem);
|
||||
});
|
||||
|
||||
// Use the event's clientX and clientY coordinates (which are viewport-relative)
|
||||
menu.style.left = x + "px";
|
||||
menu.style.top = y + "px";
|
||||
menu.style.display = "block";
|
||||
|
||||
// Adjust if the menu would extend past the bottom of the viewport
|
||||
const menuRect = menu.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
if (menuRect.bottom > viewportHeight) {
|
||||
let newTop = viewportHeight - menuRect.height;
|
||||
if (newTop < 0) newTop = 0;
|
||||
menu.style.top = newTop + "px";
|
||||
}
|
||||
}
|
||||
|
||||
function hideFileContextMenu() {
|
||||
const menu = document.getElementById("fileContextMenu");
|
||||
if (menu) {
|
||||
menu.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu handler for the file list.
|
||||
function fileListContextMenuHandler(e) {
|
||||
e.preventDefault();
|
||||
// If no file is selected, try to select the row that was right-clicked.
|
||||
let row = e.target.closest("tr");
|
||||
if (row) {
|
||||
const checkbox = row.querySelector(".file-checkbox");
|
||||
if (checkbox && !checkbox.checked) {
|
||||
checkbox.checked = true;
|
||||
updateRowHighlight(checkbox);
|
||||
updateFileActionButtons();
|
||||
}
|
||||
}
|
||||
|
||||
// Get selected file names.
|
||||
const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value);
|
||||
|
||||
// Build the context menu items.
|
||||
let menuItems = [
|
||||
{ label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } },
|
||||
{ label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } },
|
||||
{ label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } },
|
||||
{ label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } }
|
||||
];
|
||||
|
||||
if (selected.some(name => name.toLowerCase().endsWith(".zip"))) {
|
||||
menuItems.push({
|
||||
label: "Extract Zip",
|
||||
action: () => { handleExtractZipSelected(new Event("click")); }
|
||||
});
|
||||
}
|
||||
|
||||
if (selected.length === 1) {
|
||||
// Look up the file object.
|
||||
const file = fileData.find(f => f.name === selected[0]);
|
||||
|
||||
// Add Preview option.
|
||||
menuItems.push({
|
||||
label: "Preview",
|
||||
action: () => {
|
||||
const folder = window.currentFolder || "root";
|
||||
const folderPath = folder === "root"
|
||||
? "uploads/"
|
||||
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
|
||||
previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Only show Edit option if file is editable.
|
||||
if (canEditFile(file.name)) {
|
||||
menuItems.push({
|
||||
label: "Edit",
|
||||
action: () => { editFile(selected[0], window.currentFolder); }
|
||||
});
|
||||
}
|
||||
|
||||
// Add Rename option.
|
||||
menuItems.push({
|
||||
label: "Rename",
|
||||
action: () => { renameFile(selected[0], window.currentFolder); }
|
||||
});
|
||||
}
|
||||
|
||||
showFileContextMenu(e.clientX, e.clientY, menuItems);
|
||||
}
|
||||
|
||||
// Bind the context menu to the file list container.
|
||||
// (This is set every time the file list is rendered.)
|
||||
function bindFileListContextMenu() {
|
||||
const fileListContainer = document.getElementById("fileList");
|
||||
if (fileListContainer) {
|
||||
fileListContainer.oncontextmenu = fileListContextMenuHandler;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the context menu if clicking anywhere else.
|
||||
document.addEventListener("click", function(e) {
|
||||
const menu = document.getElementById("fileContextMenu");
|
||||
if (menu && menu.style.display === "block") {
|
||||
hideFileContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// After rendering the file table, bind the context menu handler.
|
||||
(function() {
|
||||
const originalRenderFileTable = renderFileTable;
|
||||
renderFileTable = function(folder) {
|
||||
originalRenderFileTable(folder);
|
||||
bindFileListContextMenu();
|
||||
};
|
||||
})();
|
||||
181
folderManager.js
@@ -1,7 +1,7 @@
|
||||
// folderManager.js
|
||||
|
||||
import { loadFileList } from './fileManager.js';
|
||||
import { showToast, escapeHTML } from './domUtils.js';
|
||||
import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js';
|
||||
|
||||
// ----------------------
|
||||
// Helper Functions (Data/State)
|
||||
@@ -63,8 +63,6 @@ function getParentFolder(folder) {
|
||||
// Breadcrumb Functions
|
||||
// ----------------------
|
||||
// Render breadcrumb for a normalized folder path.
|
||||
// For example, if window.currentFolder is "Folder1/Folder1SubFolder2",
|
||||
// this will return: Root / Folder1 / Folder1SubFolder2.
|
||||
function renderBreadcrumb(normalizedFolder) {
|
||||
if (normalizedFolder === "root") {
|
||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||
@@ -90,7 +88,6 @@ function bindBreadcrumbEvents() {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
let folder = this.getAttribute("data-folder");
|
||||
console.log("Breadcrumb clicked, folder:", folder);
|
||||
window.currentFolder = folder;
|
||||
localStorage.setItem("lastOpenedFolder", folder);
|
||||
const titleEl = document.getElementById("fileListTitle");
|
||||
@@ -308,16 +305,10 @@ export async function loadFolderTree(selectedFolder) {
|
||||
}
|
||||
|
||||
let html = `<div id="rootRow" class="root-row">
|
||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
||||
</div>`;
|
||||
if (folders.length === 0) {
|
||||
html += `<ul class="folder-tree expanded">
|
||||
<li class="folder-item">
|
||||
<span class="folder-option" data-folder="root">(Root)</span>
|
||||
</li>
|
||||
</ul>`;
|
||||
} else {
|
||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
||||
</div>`;
|
||||
if (folders.length > 0) {
|
||||
const tree = buildFolderTree(folders);
|
||||
html += renderFolderTree(tree, "", "block");
|
||||
}
|
||||
@@ -447,6 +438,7 @@ export function loadFolderList(selectedFolder) {
|
||||
// ----------------------
|
||||
|
||||
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
|
||||
|
||||
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
|
||||
|
||||
function openRenameFolderModal() {
|
||||
@@ -458,13 +450,19 @@ function openRenameFolderModal() {
|
||||
const parts = selectedFolder.split("/");
|
||||
document.getElementById("newRenameFolderName").value = parts[parts.length - 1];
|
||||
document.getElementById("renameFolderModal").style.display = "block";
|
||||
// Focus the input field after a short delay to ensure modal is visible.
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById("newRenameFolderName");
|
||||
input.focus();
|
||||
input.select();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
document.getElementById("cancelRenameFolder").addEventListener("click", function () {
|
||||
document.getElementById("renameFolderModal").style.display = "none";
|
||||
document.getElementById("newRenameFolderName").value = "";
|
||||
});
|
||||
|
||||
attachEnterKeyListener("renameFolderModal", "submitRenameFolder");
|
||||
document.getElementById("submitRenameFolder").addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
@@ -521,7 +519,7 @@ function openDeleteFolderModal() {
|
||||
document.getElementById("cancelDeleteFolder").addEventListener("click", function () {
|
||||
document.getElementById("deleteFolderModal").style.display = "none";
|
||||
});
|
||||
|
||||
attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder");
|
||||
document.getElementById("confirmDeleteFolder").addEventListener("click", function () {
|
||||
const selectedFolder = window.currentFolder || "root";
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
@@ -552,13 +550,14 @@ document.getElementById("confirmDeleteFolder").addEventListener("click", functio
|
||||
|
||||
document.getElementById("createFolderBtn").addEventListener("click", function () {
|
||||
document.getElementById("createFolderModal").style.display = "block";
|
||||
document.getElementById("newFolderName").focus();
|
||||
});
|
||||
|
||||
document.getElementById("cancelCreateFolder").addEventListener("click", function () {
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
document.getElementById("newFolderName").value = "";
|
||||
});
|
||||
|
||||
attachEnterKeyListener("createFolderModal", "submitCreateFolder");
|
||||
document.getElementById("submitCreateFolder").addEventListener("click", function () {
|
||||
const folderInput = document.getElementById("newFolderName").value.trim();
|
||||
if (!folderInput) {
|
||||
@@ -599,4 +598,150 @@ document.getElementById("submitCreateFolder").addEventListener("click", function
|
||||
console.error("Error creating folder:", error);
|
||||
document.getElementById("createFolderModal").style.display = "none";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
|
||||
|
||||
// Function to display the custom context menu at (x, y) with given menu items.
|
||||
function showFolderManagerContextMenu(x, y, menuItems) {
|
||||
let menu = document.getElementById("folderManagerContextMenu");
|
||||
if (!menu) {
|
||||
menu = document.createElement("div");
|
||||
menu.id = "folderManagerContextMenu";
|
||||
menu.style.position = "absolute";
|
||||
menu.style.padding = "5px 0";
|
||||
menu.style.minWidth = "150px";
|
||||
menu.style.zIndex = "9999";
|
||||
document.body.appendChild(menu);
|
||||
}
|
||||
|
||||
// Set styles based on dark mode.
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
menu.style.backgroundColor = "#2c2c2c";
|
||||
menu.style.border = "1px solid #555";
|
||||
menu.style.color = "#e0e0e0";
|
||||
} else {
|
||||
menu.style.backgroundColor = "#fff";
|
||||
menu.style.border = "1px solid #ccc";
|
||||
menu.style.color = "#000";
|
||||
}
|
||||
|
||||
// Clear previous items.
|
||||
menu.innerHTML = "";
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = document.createElement("div");
|
||||
menuItem.textContent = item.label;
|
||||
menuItem.style.padding = "5px 15px";
|
||||
menuItem.style.cursor = "pointer";
|
||||
menuItem.addEventListener("mouseover", () => {
|
||||
if (document.body.classList.contains("dark-mode")) {
|
||||
menuItem.style.backgroundColor = "#444";
|
||||
} else {
|
||||
menuItem.style.backgroundColor = "#f0f0f0";
|
||||
}
|
||||
});
|
||||
menuItem.addEventListener("mouseout", () => {
|
||||
menuItem.style.backgroundColor = "";
|
||||
});
|
||||
menuItem.addEventListener("click", () => {
|
||||
item.action();
|
||||
hideFolderManagerContextMenu();
|
||||
});
|
||||
menu.appendChild(menuItem);
|
||||
});
|
||||
menu.style.left = x + "px";
|
||||
menu.style.top = y + "px";
|
||||
menu.style.display = "block";
|
||||
}
|
||||
|
||||
function hideFolderManagerContextMenu() {
|
||||
const menu = document.getElementById("folderManagerContextMenu");
|
||||
if (menu) {
|
||||
menu.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu handler for folder tree and breadcrumb items.
|
||||
function folderManagerContextMenuHandler(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Get the closest folder element (either from the tree or breadcrumb).
|
||||
const target = e.target.closest(".folder-option, .breadcrumb-link");
|
||||
if (!target) return;
|
||||
|
||||
const folder = target.getAttribute("data-folder");
|
||||
if (!folder) return;
|
||||
|
||||
// Update current folder and highlight the selected element.
|
||||
window.currentFolder = folder;
|
||||
document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected"));
|
||||
target.classList.add("selected");
|
||||
|
||||
// Build context menu items.
|
||||
const menuItems = [
|
||||
{
|
||||
label: "Create Folder",
|
||||
action: () => {
|
||||
document.getElementById("createFolderModal").style.display = "block";
|
||||
document.getElementById("newFolderName").focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Rename Folder",
|
||||
action: () => { openRenameFolderModal(); }
|
||||
},
|
||||
{
|
||||
label: "Delete Folder",
|
||||
action: () => { openDeleteFolderModal(); }
|
||||
}
|
||||
];
|
||||
|
||||
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
|
||||
}
|
||||
|
||||
// Bind contextmenu events to folder tree and breadcrumb elements.
|
||||
function bindFolderManagerContextMenu() {
|
||||
// Bind context menu to folder tree container.
|
||||
const container = document.getElementById("folderTreeContainer");
|
||||
if (container) {
|
||||
container.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
container.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||
}
|
||||
|
||||
// Bind context menu to breadcrumb links.
|
||||
const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link");
|
||||
breadcrumbNodes.forEach(node => {
|
||||
node.removeEventListener("contextmenu", folderManagerContextMenuHandler);
|
||||
node.addEventListener("contextmenu", folderManagerContextMenuHandler, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide context menu when clicking elsewhere.
|
||||
document.addEventListener("click", function () {
|
||||
hideFolderManagerContextMenu();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener("keydown", function (e) {
|
||||
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
||||
const tag = e.target.tagName.toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
|
||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
||||
// Ensure a folder is selected and it isn't the root folder.
|
||||
if (window.currentFolder && window.currentFolder !== "root") {
|
||||
// Prevent default (avoid navigating back on macOS).
|
||||
e.preventDefault();
|
||||
// Call your existing folder delete function.
|
||||
openDeleteFolderModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Call this binding function after rendering the folder tree and breadcrumbs.
|
||||
bindFolderManagerContextMenu();
|
||||
415
index.html
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Multi File Upload Editor</title>
|
||||
<title>FileRise</title>
|
||||
<link rel="icon" type="image/png" href="/assets/logo.png">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
|
||||
<meta name="csrf-token" content="">
|
||||
@@ -36,9 +36,15 @@
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<!-- Drop shadow filter -->
|
||||
<!-- Drop shadow filter with animated attributes for a lifting effect -->
|
||||
<filter id="shadowFilter" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2" />
|
||||
<feDropShadow id="dropShadow" dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.2">
|
||||
<!-- Animate the vertical offset: from 2 to 1 (as it rises), hold, then back to 2 -->
|
||||
<animate attributeName="dy" values="2;1;1;2" keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||
<!-- Animate the blur similarly: from 2 to 1.5 then back to 2 -->
|
||||
<animate attributeName="stdDeviation" values="2;1.5;1.5;2" keyTimes="0;0.2;0.8;1" dur="5s"
|
||||
fill="freeze" />
|
||||
</feDropShadow>
|
||||
</filter>
|
||||
</defs>
|
||||
<style type="text/css">
|
||||
@@ -62,31 +68,32 @@
|
||||
fill: #1565C0;
|
||||
}
|
||||
</style>
|
||||
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
||||
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
||||
<!-- Divider lines for drawers -->
|
||||
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
||||
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
||||
<!-- Drawers with Handles -->
|
||||
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="27" r="1.5" class="handle" />
|
||||
|
||||
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="39" r="1.5" class="handle" />
|
||||
|
||||
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="51" r="1.5" class="handle" />
|
||||
|
||||
<!-- Additional detail: a small top handle on the cabinet door -->
|
||||
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
||||
<!-- Group that will animate upward and then back down once -->
|
||||
<g id="cabinetGroup">
|
||||
<!-- Cabinet Body with rounded corners, white outline, and drop shadow -->
|
||||
<rect x="4" y="4" width="56" height="56" rx="6" ry="6" class="cabinet" filter="url(#shadowFilter)" />
|
||||
<!-- Divider lines for drawers -->
|
||||
<line x1="5" y1="22" x2="59" y2="22" class="divider" />
|
||||
<line x1="5" y1="34" x2="59" y2="34" class="divider" />
|
||||
<!-- Drawers with Handles -->
|
||||
<rect x="8" y="24" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="27" r="1.5" class="handle" />
|
||||
<rect x="8" y="36" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="39" r="1.5" class="handle" />
|
||||
<rect x="8" y="48" width="48" height="6" rx="1" ry="1" class="drawer" />
|
||||
<circle cx="54" cy="51" r="1.5" class="handle" />
|
||||
<!-- Additional detail: a small top handle on the cabinet door -->
|
||||
<rect x="28" y="10" width="8" height="4" rx="1" ry="1" fill="#1565C0" />
|
||||
<!-- Animate transform: rises by 2 pixels over 1s, holds for 3s, then falls over 1s (total 5s) -->
|
||||
<animateTransform attributeName="transform" type="translate" values="0 0; 0 -2; 0 -2; 0 0"
|
||||
keyTimes="0;0.2;0.8;1" dur="5s" fill="freeze" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-title">
|
||||
<h1>Multi File Upload Editor</h1>
|
||||
<h1>FileRise</h1>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<div class="header-buttons">
|
||||
<button id="logoutBtn" title="Logout">
|
||||
@@ -95,7 +102,6 @@
|
||||
<button id="changePasswordBtn" title="Change Password">
|
||||
<i class="material-icons">vpn_key</i>
|
||||
</button>
|
||||
<!-- Restore Files Modal (Admin Only) -->
|
||||
<div id="restoreFilesModal" class="modal centered-modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<h4 class="custom-restore-header">
|
||||
@@ -130,198 +136,193 @@
|
||||
|
||||
<!-- Custom Toast Container -->
|
||||
<div id="customToast"></div>
|
||||
<div class="container-fluid">
|
||||
<!-- Login Form -->
|
||||
<div class="row" id="loginForm">
|
||||
<div class="col-12">
|
||||
<form id="authForm" method="post">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">User:</label>
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password:</label>
|
||||
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
|
||||
<div class="form-group remember-me-container">
|
||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||
<label for="rememberMeCheckbox">Remember me</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Operations: Upload and Folder Management -->
|
||||
<div id="mainOperations">
|
||||
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
||||
<div class="row align-items-start" id="uploadFolderRow">
|
||||
<!-- Upload Card: 50% width on medium, 58% on large -->
|
||||
<div class="col-md-6 col-lg-7 d-flex">
|
||||
<div id="uploadCard" class="card flex-fill" style="max-width: 900px; width: 100%;">
|
||||
<div class="card-header">Upload Files/Folders</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column"
|
||||
style="height: 100%;" novalidate>
|
||||
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
|
||||
<div id="uploadDropArea"
|
||||
style="border:2px dashed #ccc; padding:20px; cursor:pointer; height:100%; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
||||
<span>Drop files/folders here or click 'Choose Files'</span>
|
||||
<br />
|
||||
<!-- Note: Remove directory attributes so file picker only allows files -->
|
||||
<input type="file" id="file" name="file[]" class="form-control-file" multiple
|
||||
style="opacity:0; position:absolute; width:1px; height:1px;" />
|
||||
<button type="button" id="customChooseBtn">Choose Files</button>
|
||||
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||
<div class="main-wrapper">
|
||||
<!-- Sidebar Drop Zone: Hidden until you drag a card (display controlled by JS) -->
|
||||
<div id="sidebarDropArea" class="drop-target-sidebar"></div>
|
||||
<!-- Main Column -->
|
||||
<div id="mainColumn" class="main-column">
|
||||
<div class="container-fluid">
|
||||
<!-- Login Form (unchanged) -->
|
||||
<div class="row" id="loginForm">
|
||||
<div class="col-12">
|
||||
<form id="authForm" method="post">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">User:</label>
|
||||
<input type="text" class="form-control" id="loginUsername" name="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password:</label>
|
||||
<input type="password" class="form-control" id="loginPassword" name="password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block btn-login">Login</button>
|
||||
<div class="form-group remember-me-container">
|
||||
<input type="checkbox" id="rememberMeCheckbox" name="remember_me" />
|
||||
<label for="rememberMeCheckbox">Remember me</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Operations: Upload and Folder Management -->
|
||||
<div id="mainOperations">
|
||||
<div class="container" style="max-width: 1400px; margin: 0 auto;">
|
||||
<!-- Top Zone: Two columns (60% and 40%) -->
|
||||
<div id="uploadFolderRow" class="row">
|
||||
<!-- Left Column (60% for Upload Card) -->
|
||||
<div id="leftCol" class="col-md-7" style="display: flex; justify-content: center;">
|
||||
<div id="uploadCard" class="card" style="width: 100%;">
|
||||
<div class="card-header">Upload Files/Folders</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<form id="uploadFileForm" method="post" enctype="multipart/form-data" class="d-flex flex-column">
|
||||
<div class="form-group flex-grow-1" style="margin-bottom: 1rem;">
|
||||
<div id="uploadDropArea"
|
||||
style="border:2px dashed #ccc; padding:20px; cursor:pointer; display:flex; flex-direction:column; justify-content:center; align-items:center; position:relative;">
|
||||
<span>Drop files/folders here or click 'Choose Files'</span>
|
||||
<br />
|
||||
<input type="file" id="file" name="file[]" class="form-control-file" multiple
|
||||
style="opacity:0; position:absolute; width:1px; height:1px;" />
|
||||
<button type="button" id="customChooseBtn">Choose Files</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
|
||||
<div id="uploadProgressContainer"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column (40% for Folder Management Card) -->
|
||||
<div id="rightCol" class="col-md-5" style="display: flex; justify-content: center;">
|
||||
<div id="folderManagementCard" class="card" style="width: 100%; position: relative;">
|
||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Folder Navigation & Management</span>
|
||||
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
|
||||
style="padding: 0; border: none; background: none;">
|
||||
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body custom-folder-card-body">
|
||||
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
||||
<div id="folderTreeContainer"></div>
|
||||
</div>
|
||||
<div class="folder-actions mt-3">
|
||||
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
|
||||
<div id="createFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Create Folder</h4>
|
||||
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
|
||||
style="margin-top:10px;" />
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<div id="renameFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Rename Folder</h4>
|
||||
<input type="text" id="newRenameFolderName" class="form-control"
|
||||
placeholder="Enter new folder name" style="margin-top:10px;" />
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
<div id="deleteFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Delete Folder</h4>
|
||||
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folderHelpTooltip" class="folder-help-tooltip"
|
||||
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
|
||||
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
|
||||
<li>Click on a folder in the tree to view its files.</li>
|
||||
<li>Use [-] to collapse and [+] to expand folders.</li>
|
||||
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
|
||||
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="uploadBtn" class="btn btn-primary d-block mx-auto">Upload</button>
|
||||
<div id="uploadProgressContainer"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end uploadFolderRow -->
|
||||
</div> <!-- end container -->
|
||||
</div> <!-- end mainOperations -->
|
||||
|
||||
<!-- File List Section -->
|
||||
<div id="fileListContainer" style="display: none;">
|
||||
<h2 id="fileListTitle">Files in (Root)</h2>
|
||||
<div id="fileListActions" class="file-list-actions">
|
||||
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
|
||||
<div id="deleteFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Delete Selected Files</h4>
|
||||
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folder Management Card -->
|
||||
<div class="col-md-6 col-lg-5 d-flex">
|
||||
<div id="folderManagementCard" class="card flex-fill"
|
||||
style="max-width: 900px; width: 100%; position: relative;">
|
||||
<!-- Card header with folder management title and help icon -->
|
||||
<div class="card-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Folder Navigation & Management</span>
|
||||
<button id="folderHelpBtn" class="btn btn-link" title="Folder Help"
|
||||
style="padding: 0; border: none; background: none;">
|
||||
<i class="material-icons folder-help-icon" style="font-size: 24px;">info</i>
|
||||
</button>
|
||||
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
|
||||
<div id="copyFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Copy Selected Files</h4>
|
||||
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
|
||||
<select id="copyTargetFolder" class="form-control modal-input"></select>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body custom-folder-card-body">
|
||||
<div class="form-group d-flex align-items-top" style="padding-top:0; margin-bottom:0;">
|
||||
<div id="folderTreeContainer"></div>
|
||||
</div>
|
||||
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
|
||||
<div id="moveFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Move Selected Files</h4>
|
||||
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
|
||||
<select id="moveTargetFolder" class="form-control modal-input"></select>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
|
||||
</div>
|
||||
<!-- Folder actions (create, rename, delete) -->
|
||||
<div class="folder-actions mt-3">
|
||||
<button id="createFolderBtn" class="btn btn-primary">Create Folder</button>
|
||||
<!-- Create Folder Modal -->
|
||||
<div id="createFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Create Folder</h4>
|
||||
<input type="text" id="newFolderName" class="form-control" placeholder="Enter folder name"
|
||||
style="margin-top:10px;" />
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelCreateFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="submitCreateFolder" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="renameFolderBtn" class="btn btn-secondary ml-2" title="Rename Folder">
|
||||
<i class="material-icons">drive_file_rename_outline</i>
|
||||
</button>
|
||||
<!-- Rename Folder Modal -->
|
||||
<div id="renameFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Rename Folder</h4>
|
||||
<input type="text" id="newRenameFolderName" class="form-control"
|
||||
placeholder="Enter new folder name" style="margin-top:10px;" />
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelRenameFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="submitRenameFolder" class="btn btn-primary">Rename</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="deleteFolderBtn" class="btn btn-danger ml-2" title="Delete Folder">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
<!-- Delete Folder Modal -->
|
||||
<div id="deleteFolderModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Delete Folder</h4>
|
||||
<p id="deleteFolderMessage">Are you sure you want to delete this folder?</p>
|
||||
<div style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelDeleteFolder" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDeleteFolder" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Help Tooltip: Initially hidden -->
|
||||
<div id="folderHelpTooltip" class="folder-help-tooltip"
|
||||
style="display: none; position: absolute; top: 50px; right: 15px; background: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1000; box-shadow: 2px 2px 6px rgba(0,0,0,0.2);">
|
||||
<ul class="folder-help-list" style="margin: 0; padding-left: 20px;">
|
||||
<li>Click on a folder in the tree to view its files.</li>
|
||||
<li>Use [-] to collapse and [+] to expand folders.</li>
|
||||
<li>Select a folder and click "Create Folder" to add a subfolder.</li>
|
||||
<li>To rename or delete a folder, select it and then click the appropriate button.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
|
||||
<button id="extractZipBtn" class="btn btn-sm btn-info" title="Extract Zip">Extract Zip</button>
|
||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4>Download Selected Files as Zip</h4>
|
||||
<p>Enter a name for the zip file:</p>
|
||||
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
|
||||
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- File List Section -->
|
||||
<div id="fileListContainer" style="display: none;">
|
||||
<h2 id="fileListTitle">Files in (Root)</h2>
|
||||
<div id="fileListActions" class="file-list-actions">
|
||||
<button id="deleteSelectedBtn" class="btn action-btn" style="display: none;">Delete Files</button>
|
||||
<!-- Delete Files Modal -->
|
||||
<div id="deleteFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Delete Selected Files</h4>
|
||||
<p id="deleteFilesMessage">Are you sure you want to delete the selected files?</p>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelDeleteFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDeleteFiles" class="btn btn-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end container-fluid -->
|
||||
</div> <!-- end mainColumn -->
|
||||
</div> <!-- end main-wrapper -->
|
||||
|
||||
<button id="copySelectedBtn" class="btn action-btn" style="display: none;" disabled>Copy Files</button>
|
||||
<!-- Copy Files Modal -->
|
||||
<div id="copyFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Copy Selected Files</h4>
|
||||
<p id="copyFilesMessage">Select a target folder for copying the selected files:</p>
|
||||
<select id="copyTargetFolder" class="form-control modal-input"></select>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelCopyFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmCopyFiles" class="btn btn-primary">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="moveSelectedBtn" class="btn action-btn" style="display: none;" disabled>Move Files</button>
|
||||
<!-- Move Files Modal -->
|
||||
<div id="moveFilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Move Selected Files</h4>
|
||||
<p id="moveFilesMessage">Select a target folder for moving the selected files:</p>
|
||||
<select id="moveTargetFolder" class="form-control modal-input"></select>
|
||||
<div class="modal-footer">
|
||||
<button id="cancelMoveFiles" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmMoveFiles" class="btn btn-primary">Move</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="downloadZipBtn" class="btn action-btn" style="display: none;" disabled>Download ZIP</button>
|
||||
<!-- Download Zip Modal -->
|
||||
<div id="downloadZipModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h4>Download Selected Files as Zip</h4>
|
||||
<p>Enter a name for the zip file:</p>
|
||||
<input type="text" id="zipFileNameInput" class="form-control" placeholder="files.zip" />
|
||||
<div class="modal-footer" style="margin-top:15px; text-align:right;">
|
||||
<button id="cancelDownloadZip" class="btn btn-secondary">Cancel</button>
|
||||
<button id="confirmDownloadZip" class="btn btn-primary">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Password-->
|
||||
<!-- Change Password, Add User, Remove User, Rename File, and Custom Confirm Modals (unchanged) -->
|
||||
<div id="changePasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:400px; margin:auto;">
|
||||
<span id="closeChangePasswordModal" style="cursor:pointer;">×</span>
|
||||
@@ -333,8 +334,6 @@
|
||||
<button id="saveNewPasswordBtn" class="btn btn-primary" style="width:100%;">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add User Modal -->
|
||||
<div id="addUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Create New User</h3>
|
||||
@@ -352,8 +351,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remove User Modal -->
|
||||
<div id="removeUserModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3>Remove User</h3>
|
||||
@@ -365,8 +362,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rename File Modal -->
|
||||
<div id="renameFileModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h4>Rename File</h4>
|
||||
@@ -378,8 +373,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Confirm Modal -->
|
||||
<div id="customConfirmModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<p id="confirmMessage"></p>
|
||||
|
||||
34
logout.php
@@ -1,19 +1,37 @@
|
||||
<?php
|
||||
session_start();
|
||||
require 'config.php';
|
||||
|
||||
// Retrieve headers and check CSRF token.
|
||||
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
|
||||
$receivedToken = isset($headers['x-csrf-token']) ? trim($headers['x-csrf-token']) : '';
|
||||
|
||||
// Fallback: If a CSRF token exists in the session and doesn't match the one provided,
|
||||
// log the mismatch but proceed with logout.
|
||||
// If there's a mismatch, log it but continue with logout.
|
||||
if (isset($_SESSION['csrf_token']) && $receivedToken !== $_SESSION['csrf_token']) {
|
||||
// Optionally log this event:
|
||||
error_log("CSRF token mismatch on logout. Proceeding with logout.");
|
||||
}
|
||||
|
||||
$_SESSION = []; // Clear session data
|
||||
session_destroy(); // Destroy session
|
||||
// If the remember me token is set, remove it from the persistent tokens file.
|
||||
if (isset($_COOKIE['remember_me_token'])) {
|
||||
$token = $_COOKIE['remember_me_token'];
|
||||
$persistentTokensFile = USERS_DIR . 'persistent_tokens.json';
|
||||
if (file_exists($persistentTokensFile)) {
|
||||
$encryptedContent = file_get_contents($persistentTokensFile);
|
||||
$decryptedContent = decryptData($encryptedContent, $encryptionKey);
|
||||
$persistentTokens = json_decode($decryptedContent, true);
|
||||
if (is_array($persistentTokens) && isset($persistentTokens[$token])) {
|
||||
unset($persistentTokens[$token]);
|
||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||
file_put_contents($persistentTokensFile, $newEncryptedContent, LOCK_EX);
|
||||
}
|
||||
}
|
||||
// Clear the cookie.
|
||||
setcookie('remember_me_token', '', time() - 3600, '/', '', $secure, true);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["success" => "Logged out"]);
|
||||
// Clear session data and destroy the session.
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
|
||||
header("Location: index.html");
|
||||
exit;
|
||||
?>
|
||||
11
main.js
@@ -17,6 +17,7 @@ import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder } from './dragAndDrop.js'
|
||||
|
||||
function loadCsrfToken() {
|
||||
fetch('token.php', { credentials: 'include' })
|
||||
@@ -64,6 +65,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
// Call initAuth synchronously.
|
||||
initAuth();
|
||||
|
||||
const newPasswordInput = document.getElementById("newPassword");
|
||||
if (newPasswordInput) {
|
||||
newPasswordInput.addEventListener("input", function() {
|
||||
console.log("newPassword input event:", this.value);
|
||||
});
|
||||
} else {
|
||||
console.error("newPassword input not found!");
|
||||
}
|
||||
// --- Dark Mode Persistence ---
|
||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||
const storedDarkMode = localStorage.getItem("darkMode");
|
||||
@@ -121,6 +130,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (authenticated) {
|
||||
window.currentFolder = "root";
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// networkUtils.js
|
||||
export function sendRequest(url, method = "GET", data = null) {
|
||||
console.log("Sending request to:", url, "with method:", method);
|
||||
const options = {
|
||||
@@ -24,9 +23,11 @@ export function sendRequest(url, method = "GET", data = null) {
|
||||
throw new Error(`HTTP error ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
// Clone the response so we can safely fall back if JSON parsing fails.
|
||||
const clonedResponse = response.clone();
|
||||
return response.json().catch(() => {
|
||||
console.warn("Response is not JSON, returning as text");
|
||||
return response.text();
|
||||
return clonedResponse.text();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 626 KiB |
BIN
resources/dark-gallery.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
resources/dark-login.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
resources/dark-mode-default.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 400 KiB |
BIN
resources/light-preview.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
resources/light-share.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
resources/light-trash.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 416 KiB |
350
styles.css
@@ -25,22 +25,15 @@ body {
|
||||
/* CONTAINER */
|
||||
.container,
|
||||
.container-fluid {
|
||||
margin-top: 20px;
|
||||
margin-top: 10px;
|
||||
padding-right: 4px !important;
|
||||
padding-left: 4px !important;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@media (min-width: 1300px) {
|
||||
.container-fluid {
|
||||
padding-left: 50px !important;
|
||||
padding-right: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.container-fluid {
|
||||
padding-left: 100px !important;
|
||||
padding-right: 100px !important;
|
||||
padding-left: 40px !important;
|
||||
padding-right: 40px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +45,6 @@ body {
|
||||
/* FLEXBOX HEADER: LOGO, TITLE, BUTTONS FIXED */
|
||||
/************************************************************/
|
||||
|
||||
|
||||
#uploadCard,
|
||||
#folderManagementCard {
|
||||
min-height: 342px;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -216,6 +203,7 @@ body.dark-mode header {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Folder Help Tooltip - Light Mode */
|
||||
.folder-help-tooltip {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
@@ -225,6 +213,7 @@ body.dark-mode header {
|
||||
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Folder Help Tooltip - Dark Mode */
|
||||
body.dark-mode .folder-help-tooltip {
|
||||
background-color: #333 !important;
|
||||
color: #eee !important;
|
||||
@@ -234,17 +223,19 @@ body.dark-mode .folder-help-tooltip {
|
||||
#folderHelpBtn i.material-icons.folder-help-icon {
|
||||
-webkit-text-fill-color: orange !important;
|
||||
color: inherit !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
||||
-webkit-text-fill-color: #ffa500 !important;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
|
||||
/************************************************************/
|
||||
/* RESPONSIVE HEADER FIXES */
|
||||
/************************************************************/
|
||||
|
||||
@media (max-width: 970px) {
|
||||
@media (max-width: 790px) {
|
||||
.header-container {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
@@ -284,7 +275,7 @@ body.dark-mode #folderHelpBtn i.material-icons.folder-help-icon {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,21 +368,6 @@ body.dark-mode #loginForm button:hover {
|
||||
CARDS & MODALS
|
||||
=========================================================== */
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
#restoreFilesModal .modal-content {
|
||||
position: fixed !important;
|
||||
top: 50% !important;
|
||||
@@ -713,12 +689,25 @@ body.dark-mode .editor-header {
|
||||
|
||||
.material-icons.pauseResumeBtn {
|
||||
color: black !important;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
body.dark-mode .material-icons.pauseResumeBtn {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
body.dark-mode .material-icons.pauseResumeBtn:hover {
|
||||
background-color: rgba(255, 215, 0, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#uploadProgressContainer ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -1008,29 +997,6 @@ label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#uploadFolderRow {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#uploadFolderRow .col-md-6 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#uploadFolderRow .col-md-6:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card-body .form-group {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#createFolderBtn {
|
||||
margin-top: 0px !important;
|
||||
height: 40px !important;
|
||||
@@ -1085,34 +1051,6 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.folder-option:hover {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.folder-option.selected {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option.selected {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.custom-folder-card-body {
|
||||
padding-top: 5px !important;
|
||||
}
|
||||
|
||||
#customToast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
@@ -1160,8 +1098,10 @@ body.dark-mode .folder-option:hover {
|
||||
|
||||
#fileListContainer {
|
||||
max-width: 100%;
|
||||
padding: 10px 5px;
|
||||
margin: 20px auto;
|
||||
padding-bottom: 10px !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 750px) {
|
||||
@@ -1175,11 +1115,6 @@ body.dark-mode #fileListContainer {
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
padding-top: 10px !important;
|
||||
padding-bottom: 10px !important;
|
||||
padding-left: 5px !important;
|
||||
padding-right: 5px !important;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#fileListContainer>h2,
|
||||
@@ -1209,6 +1144,7 @@ body.dark-mode #fileListContainer {
|
||||
|
||||
#fileListTitle {
|
||||
font-size: 1.8em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@@ -1269,6 +1205,7 @@ body.dark-mode #fileListContainer {
|
||||
/* ===========================================================
|
||||
FOLDER TREE STYLES
|
||||
=========================================================== */
|
||||
/* Make breadcrumb links look clickable */
|
||||
.breadcrumb-link {
|
||||
cursor: pointer;
|
||||
color: #007bff;
|
||||
@@ -1318,15 +1255,38 @@ body.dark-mode #fileListContainer {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.folder-option {
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
#folderTreeContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.folder-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.folder-option:hover {
|
||||
background-color: #f0f0f0;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.folder-option.selected {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option.selected {
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
body.dark-mode .folder-option:hover {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* ===========================================================
|
||||
FILE MANAGER INLINE STYLE REMOVAL - New Classes
|
||||
=========================================================== */
|
||||
@@ -1385,7 +1345,6 @@ body.dark-mode .image-preview-modal-content {
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
/* Your custom styles here */
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
@@ -1858,4 +1817,199 @@ body.dark-mode .drop-hover {
|
||||
|
||||
#restoreFilesList li label {
|
||||
margin-left: 8px !important;
|
||||
}
|
||||
|
||||
body.dark-mode #fileContextMenu {
|
||||
background-color: #2c2c2c !important;
|
||||
border: 1px solid #555 !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
body.dark-mode #fileContextMenu div {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
#folderContextMenu {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
body.dark-mode #folderContextMenu {
|
||||
background-color: #2c2c2c;
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.drop-target-sidebar {
|
||||
display: none;
|
||||
width: 50px;
|
||||
transition: width 0.3s ease;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 2px dashed #1565C0;
|
||||
padding: 10px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.drop-target-sidebar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.drop-target-sidebar.active {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.main-column {
|
||||
flex: 1;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
#uploadFolderRow {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#uploadFolderRow {
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
#leftCol,
|
||||
#rightCol {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 370px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
#leftCol {
|
||||
flex: 0 1 60%;
|
||||
}
|
||||
#rightCol {
|
||||
flex: 0 1 40%;
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
.drop-target-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
#uploadFolderRow {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
#leftCol, #rightCol {
|
||||
flex: 0 1 100% !important;
|
||||
}
|
||||
#rightCol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebarDropArea.highlight,
|
||||
#uploadFolderRow.highlight {
|
||||
border: 2px dashed #1565C0;
|
||||
background-color: #eef;
|
||||
}
|
||||
|
||||
.drag-header {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drag-header::after {
|
||||
content: '⋮⋮';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 16px;
|
||||
color: #1565C0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#uploadCard,
|
||||
#folderManagementCard {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
min-height: 353px;
|
||||
}
|
||||
|
||||
|
||||
#uploadFolderRow.highlight {
|
||||
min-height: 353px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#sidebarDropArea,
|
||||
#uploadFolderRow {
|
||||
background-color: transparent;
|
||||
}
|
||||
#sidebarDropArea {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.dark-mode #sidebarDropArea,
|
||||
body.dark-mode #uploadFolderRow {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
body.dark-mode #sidebarDropArea.highlight,
|
||||
body.dark-mode #uploadFolderRow.highlight {
|
||||
background-color: #333;
|
||||
border: 2px dashed #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.drop-target-sidebar.highlight {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.drop-target-sidebar:not(.highlight) {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.dragging:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#sidebarDropArea > .card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ddd;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.custom-folder-card-body {
|
||||
padding-top: 5px !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
@@ -4,20 +4,13 @@ import { toggleVisibility, showToast } from './domUtils.js';
|
||||
import { loadFileList } from './fileManager.js';
|
||||
import { loadFolderTree } from './folderManager.js';
|
||||
|
||||
/**
|
||||
* Displays a custom confirmation modal with the given message.
|
||||
* Calls onConfirm() if the user confirms.
|
||||
*/
|
||||
function showConfirm(message, onConfirm) {
|
||||
// Assume your custom confirm modal exists with id "customConfirmModal"
|
||||
// and has elements "confirmMessage", "confirmYesBtn", and "confirmNoBtn".
|
||||
const modal = document.getElementById("customConfirmModal");
|
||||
const messageElem = document.getElementById("confirmMessage");
|
||||
const yesBtn = document.getElementById("confirmYesBtn");
|
||||
const noBtn = document.getElementById("confirmNoBtn");
|
||||
|
||||
if (!modal || !messageElem || !yesBtn || !noBtn) {
|
||||
// Fallback to browser confirm if custom modal is not found.
|
||||
if (confirm(message)) {
|
||||
onConfirm();
|
||||
}
|
||||
@@ -42,10 +35,6 @@ function showConfirm(message, onConfirm) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for trash restore and delete operations.
|
||||
* This function should be called from main.js after authentication.
|
||||
*/
|
||||
export function setupTrashRestoreDelete() {
|
||||
|
||||
// --- Attach listener to the restore button (created in auth.js) to open the modal.
|
||||
|
||||