Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d678ee19 | ||
|
|
da62e70c02 | ||
|
|
f19d30f58a | ||
|
|
a8202adbec | ||
|
|
5dc58ffa42 | ||
|
|
f4f700ecda | ||
|
|
94178775d5 | ||
|
|
1d3f731483 | ||
|
|
6926d5b065 | ||
|
|
46e9761cae | ||
|
|
fa828f5dea | ||
|
|
3a86903827 | ||
|
|
4feef5700d | ||
|
|
41e2b5af90 | ||
|
|
27f071ba6e | ||
|
|
9020251ed5 | ||
|
|
84822e699e | ||
|
|
3d57efba6c |
37
README.md
@@ -1,11 +1,16 @@
|
|||||||
# FileRise - Elevate your File Management
|
# FileRise - Elevate your File Management
|
||||||
|
|
||||||
**Video demo:**
|
**Demo link:** https://demo.filerise.net
|
||||||
|
**UserName:** demo
|
||||||
|
**Password:** demo
|
||||||
|
Read only permissions but can view the interface.
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/9546a76b-afb0-4068-875a-0eab478b514d
|
**4/3/2025 Video demo:**
|
||||||
|
|
||||||
|
https://github.com/user-attachments/assets/221f6a53-85f5-48d4-9abe-89445e0af90e
|
||||||
|
|
||||||
**Dark mode:**
|
**Dark mode:**
|
||||||

|

|
||||||
|
|
||||||
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
||||||
|
|
||||||
@@ -147,6 +152,10 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
|||||||
- **Top Bar Drop Zone:**
|
- **Top Bar Drop Zone:**
|
||||||
- A top drop zone is available for reordering or managing cards quickly.
|
- 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.
|
- Dragging a card to the top drop zone provides immediate visual feedback, ensuring a fluid and customizable workflow.
|
||||||
|
- **Header Drop Zone with State Preservation:**
|
||||||
|
- Cards can be dragged into the header drop zone, where they are represented by a compact material icon.
|
||||||
|
- **State Preservation:** Instead of removing the card from the DOM, the original card is moved into a hidden container. This ensures that dynamic features (such as the folder tree in the Folder Management card or file selection in the Upload card) remain fully initialized and retain their state on page refresh.
|
||||||
|
- **Modal Display:** When the user interacts (via hover or click) with the header icon, the card is temporarily moved into a modal overlay for full interaction. When the modal is closed, the card is returned to the hidden container, keeping its state persistent.
|
||||||
- **Seamless Interaction:**
|
- **Seamless Interaction:**
|
||||||
- Both drop zones support smooth drag-and-drop interactions with animations and pointer event adjustments, ensuring reliable card placement regardless of screen position.
|
- Both drop zones support smooth drag-and-drop interactions with animations and pointer event adjustments, ensuring reliable card placement regardless of screen position.
|
||||||
|
|
||||||
@@ -170,23 +179,23 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
|||||||
- Features an intuitive interface with Material Icons for quick recognition and access.
|
- Features an intuitive interface with Material Icons for quick recognition and access.
|
||||||
- Allows administrators to manage authentication settings, user management, and login methods in real time.
|
- Allows administrators to manage authentication settings, user management, and login methods in real time.
|
||||||
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
|
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
|
||||||
- User Permissions options
|
- **User Permissions Options:**
|
||||||
- Folder Only gives user their own root folder
|
- *Folder Only* gives user their own root folder.
|
||||||
- Read Only makes it so user can only read the files
|
- *Read Only* makes it so the user can only read the files.
|
||||||
- Disable upload
|
- *Disable Upload* prevents file uploads.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
**Light mode:**
|
**Admin Panel:**
|
||||||

|

|
||||||
|
|
||||||
**Light mode:**
|
**Light mode:**
|
||||||

|

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

|

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

|

|
||||||
@@ -197,8 +206,8 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
|||||||
**Restore or Delete Trash:**
|
**Restore or Delete Trash:**
|
||||||

|

|
||||||
|
|
||||||
**Dark Login page:**
|
**Dark TOTP Setup:**
|
||||||

|

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

|

|
||||||
|
|||||||
7
auth.js
@@ -400,6 +400,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
disableBasicAuth: localStorage.getItem("disableBasicAuth") === "true",
|
||||||
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
disableOIDCLogin: localStorage.getItem("disableOIDCLogin") === "true"
|
||||||
});
|
});
|
||||||
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
||||||
|
if (oidcLoginBtn) {
|
||||||
|
oidcLoginBtn.addEventListener("click", () => {
|
||||||
|
// Redirect to the OIDC auth endpoint. The endpoint can be adjusted if needed.
|
||||||
|
window.location.href = "auth.php?oidc=initiate";
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export { initAuth, checkAuthentication };
|
export { initAuth, checkAuthentication };
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { showToast, toggleVisibility } from './domUtils.js';
|
import { showToast, toggleVisibility } from './domUtils.js';
|
||||||
import { sendRequest } from './networkUtils.js';
|
import { sendRequest } from './networkUtils.js';
|
||||||
|
|
||||||
const version = "v1.0.5";
|
const version = "v1.0.7";
|
||||||
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
const adminTitle = `Admin Panel <small style="font-size: 12px; color: gray;">${version}</small>`;
|
||||||
let lastLoginData = null;
|
let lastLoginData = null;
|
||||||
|
|
||||||
|
|||||||
17
config.php
@@ -55,7 +55,7 @@ if (!$encryptionKey) {
|
|||||||
|
|
||||||
function loadUserPermissions($username)
|
function loadUserPermissions($username)
|
||||||
{
|
{
|
||||||
global $encryptionKey; // Ensure $encryptionKey is available
|
global $encryptionKey;
|
||||||
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
$permissionsFile = USERS_DIR . 'userPermissions.json';
|
||||||
|
|
||||||
if (file_exists($permissionsFile)) {
|
if (file_exists($permissionsFile)) {
|
||||||
@@ -69,21 +69,12 @@ function loadUserPermissions($username)
|
|||||||
$permissions = json_decode($content, true);
|
$permissions = json_decode($content, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_array($permissions)) {
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($permissions) && array_key_exists($username, $permissions)) {
|
if (is_array($permissions) && array_key_exists($username, $permissions)) {
|
||||||
$result = $permissions[$username];
|
$result = $permissions[$username];
|
||||||
if (empty($result)) {
|
return !empty($result) ? $result : false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
error_log("loadUserPermissions: Permissions file not found: $permissionsFile");
|
|
||||||
}
|
}
|
||||||
|
// Removed error_log() to prevent flooding logs when file is not found.
|
||||||
return false; // Return false if no permissions found.
|
return false; // Return false if no permissions found.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +123,7 @@ if (!isset($_SESSION["authenticated"]) && isset($_COOKIE['remember_me_token']))
|
|||||||
$_SESSION["authenticated"] = true;
|
$_SESSION["authenticated"] = true;
|
||||||
$_SESSION["username"] = $tokenData["username"];
|
$_SESSION["username"] = $tokenData["username"];
|
||||||
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
// IMPORTANT: Set the folderOnly flag here for auto-login.
|
||||||
$_SESSION["folderOnly"] = loadFolderPermission($tokenData["username"]);
|
$_SESSION["folderOnly"] = loadUserPermissions($tokenData["username"]);
|
||||||
} else {
|
} else {
|
||||||
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
unset($persistentTokens[$_COOKIE['remember_me_token']]);
|
||||||
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
$newEncryptedContent = encryptData(json_encode($persistentTokens, JSON_PRETTY_PRINT), $encryptionKey);
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
@@ -136,11 +136,11 @@ export function buildFileTableRow(file, folderPath) {
|
|||||||
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
const safeUploader = escapeHTML(file.uploader || "Unknown");
|
||||||
|
|
||||||
let previewButton = "";
|
let previewButton = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(file.name)) {
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|mp3|wav|m4a|ogg|flac|aac|wma|opus|mkv|ogv)$/i.test(file.name)) {
|
||||||
let previewIcon = "";
|
let previewIcon = "";
|
||||||
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">image</i>`;
|
previewIcon = `<i class="material-icons">image</i>`;
|
||||||
} else if (/\.(mp4|webm|mov)$/i.test(file.name)) {
|
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">videocam</i>`;
|
previewIcon = `<i class="material-icons">videocam</i>`;
|
||||||
} else if (/\.pdf$/i.test(file.name)) {
|
} else if (/\.pdf$/i.test(file.name)) {
|
||||||
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
previewIcon = `<i class="material-icons">picture_as_pdf</i>`;
|
||||||
|
|||||||
60
download.php
@@ -1,8 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'config.php';
|
require_once 'config.php';
|
||||||
|
|
||||||
// For GET requests (which download.php will use), we assume session authentication is enough.
|
|
||||||
|
|
||||||
// Check if the user is authenticated.
|
// Check if the user is authenticated.
|
||||||
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
@@ -22,38 +20,70 @@ if (!preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $file)) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the directory.
|
// Get the realpath of the upload directory.
|
||||||
if ($folder !== 'root') {
|
$uploadDirReal = realpath(UPLOAD_DIR);
|
||||||
$directory = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR;
|
if ($uploadDirReal === false) {
|
||||||
} else {
|
http_response_code(500);
|
||||||
$directory = UPLOAD_DIR;
|
echo json_encode(["error" => "Server misconfiguration."]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$filePath = $directory . $file;
|
// Determine the directory.
|
||||||
|
if ($folder === 'root') {
|
||||||
|
$directory = $uploadDirReal;
|
||||||
|
} else {
|
||||||
|
// Prevent path traversal in folder parameter.
|
||||||
|
if (strpos($folder, '..') !== false) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Invalid folder name."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directoryPath = rtrim(UPLOAD_DIR, '/\\') . DIRECTORY_SEPARATOR . $folder;
|
||||||
|
$directory = realpath($directoryPath);
|
||||||
|
|
||||||
|
// Ensure that the resolved directory exists and is within the allowed UPLOAD_DIR.
|
||||||
|
if ($directory === false || strpos($directory, $uploadDirReal) !== 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(["error" => "Invalid folder path."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!file_exists($filePath)) {
|
// Build the file path.
|
||||||
|
$filePath = $directory . DIRECTORY_SEPARATOR . $file;
|
||||||
|
$realFilePath = realpath($filePath);
|
||||||
|
|
||||||
|
// Validate that the real file path exists and is within the allowed directory.
|
||||||
|
if ($realFilePath === false || strpos($realFilePath, $uploadDirReal) !== 0) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(["error" => "Access forbidden."]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($realFilePath)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(["error" => "File not found."]);
|
echo json_encode(["error" => "File not found."]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve the file.
|
// Serve the file.
|
||||||
$mimeType = mime_content_type($filePath);
|
$mimeType = mime_content_type($realFilePath);
|
||||||
header("Content-Type: " . $mimeType);
|
header("Content-Type: " . $mimeType);
|
||||||
|
|
||||||
// For images, serve inline; for other types, force download.
|
// For images, serve inline; for other types, force download.
|
||||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($realFilePath, PATHINFO_EXTENSION));
|
||||||
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
if (in_array($ext, ['jpg','jpeg','png','gif','bmp','webp','svg','ico'])) {
|
||||||
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');
|
header('Content-Disposition: inline; filename="' . basename($realFilePath) . '"');
|
||||||
} else {
|
} else {
|
||||||
header('Content-Disposition: attachment; filename="' . basename($filePath) . '"');
|
header('Content-Disposition: attachment; filename="' . basename($realFilePath) . '"');
|
||||||
}
|
}
|
||||||
header('Content-Length: ' . filesize($filePath));
|
header('Content-Length: ' . filesize($realFilePath));
|
||||||
|
|
||||||
// Disable caching.
|
// Disable caching.
|
||||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||||
header('Pragma: no-cache');
|
header('Pragma: no-cache');
|
||||||
|
|
||||||
readfile($filePath);
|
readfile($realFilePath);
|
||||||
exit;
|
exit;
|
||||||
?>
|
?>
|
||||||
819
dragAndDrop.js
@@ -1,364 +1,599 @@
|
|||||||
// dragAndDrop.js
|
// dragAndDrop.js
|
||||||
|
// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones.
|
||||||
|
// It also manages the visibility of the sidebar and header drop zones based on the current state of the application.
|
||||||
|
// It includes functions to save and load the order of cards in the sidebar and header from localStorage.
|
||||||
|
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones.
|
||||||
|
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
|
||||||
|
|
||||||
// Moves cards into the sidebar based on the saved order in localStorage.
|
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||||
export function loadSidebarOrder() {
|
export function loadSidebarOrder() {
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
const orderStr = localStorage.getItem('sidebarOrder');
|
const orderStr = localStorage.getItem('sidebarOrder');
|
||||||
if (orderStr) {
|
if (orderStr) {
|
||||||
const order = JSON.parse(orderStr);
|
const order = JSON.parse(orderStr);
|
||||||
if (order.length > 0) {
|
if (order.length > 0) {
|
||||||
// Ensure main wrapper is visible.
|
// Ensure main wrapper is visible.
|
||||||
const mainWrapper = document.querySelector('.main-wrapper');
|
const mainWrapper = document.querySelector('.main-wrapper');
|
||||||
if (mainWrapper) {
|
if (mainWrapper) {
|
||||||
mainWrapper.style.display = 'flex';
|
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);
|
||||||
}
|
}
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
updateSidebarVisibility();
|
|
||||||
}
|
// NEW: Load header order from localStorage.
|
||||||
|
export function loadHeaderOrder() {
|
||||||
// Internal helper: update sidebar visibility based on its content.
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
function updateSidebarVisibility() {
|
if (!headerDropArea) return;
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const orderStr = localStorage.getItem('headerOrder');
|
||||||
if (sidebar) {
|
if (orderStr) {
|
||||||
|
const order = JSON.parse(orderStr);
|
||||||
|
if (order.length > 0) {
|
||||||
|
order.forEach(id => {
|
||||||
|
const card = document.getElementById(id);
|
||||||
|
// Only load if card is not already in header drop zone.
|
||||||
|
if (card && card.parentNode.id !== 'headerDropArea') {
|
||||||
|
insertCardInHeader(card, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal helper: update sidebar visibility based on its content.
|
||||||
|
function updateSidebarVisibility() {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
if (cards.length > 0) {
|
if (cards.length > 0) {
|
||||||
sidebar.classList.add('active');
|
sidebar.classList.add('active');
|
||||||
sidebar.style.display = 'block';
|
sidebar.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
sidebar.classList.remove('active');
|
sidebar.classList.remove('active');
|
||||||
sidebar.style.display = 'none';
|
sidebar.style.display = 'none';
|
||||||
}
|
}
|
||||||
// Save the current order in localStorage.
|
// Save the current order in localStorage.
|
||||||
saveSidebarOrder();
|
saveSidebarOrder();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// NEW: Save header order to localStorage.
|
||||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
function saveHeaderOrder() {
|
||||||
function updateTopZoneLayout() {
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
const leftCol = document.getElementById('leftCol');
|
if (headerDropArea) {
|
||||||
const rightCol = document.getElementById('rightCol');
|
const icons = Array.from(headerDropArea.children);
|
||||||
|
// Each header icon stores its associated card in the property cardElement.
|
||||||
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
const order = icons.map(icon => icon.cardElement.id);
|
||||||
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
localStorage.setItem('headerOrder', JSON.stringify(order));
|
||||||
|
}
|
||||||
if (leftIsEmpty && !rightIsEmpty) {
|
}
|
||||||
|
|
||||||
|
// 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';
|
leftCol.style.display = 'none';
|
||||||
rightCol.style.margin = '0 auto';
|
rightCol.style.margin = '0 auto';
|
||||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||||
rightCol.style.display = 'none';
|
rightCol.style.display = 'none';
|
||||||
leftCol.style.margin = '0 auto';
|
leftCol.style.margin = '0 auto';
|
||||||
} else {
|
} else {
|
||||||
leftCol.style.display = '';
|
leftCol.style.display = '';
|
||||||
rightCol.style.display = '';
|
rightCol.style.display = '';
|
||||||
leftCol.style.margin = '';
|
leftCol.style.margin = '';
|
||||||
rightCol.style.margin = '';
|
rightCol.style.margin = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
function addTopZoneHighlight() {
|
||||||
function addTopZoneHighlight() {
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
const topZone = document.getElementById('uploadFolderRow');
|
if (topZone) {
|
||||||
if (topZone) {
|
|
||||||
topZone.classList.add('highlight');
|
topZone.classList.add('highlight');
|
||||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||||
topZone.style.minHeight = '375px';
|
topZone.style.minHeight = '375px';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// When the drag ends, remove the extra min-height.
|
||||||
// When the drag ends, remove the extra min-height.
|
function removeTopZoneHighlight() {
|
||||||
function removeTopZoneHighlight() {
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
const topZone = document.getElementById('uploadFolderRow');
|
if (topZone) {
|
||||||
if (topZone) {
|
|
||||||
topZone.classList.remove('highlight');
|
topZone.classList.remove('highlight');
|
||||||
topZone.style.minHeight = '';
|
topZone.style.minHeight = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Vertical slide/fade animation helper.
|
||||||
// Vertical slide/fade animation helper.
|
function animateVerticalSlide(card) {
|
||||||
function animateVerticalSlide(card) {
|
card.style.transform = 'translateY(30px)';
|
||||||
card.style.transform = 'translateY(30px)';
|
card.style.opacity = '0';
|
||||||
card.style.opacity = '0';
|
// Force reflow.
|
||||||
// Force reflow.
|
card.offsetWidth;
|
||||||
card.offsetWidth;
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||||
card.style.transform = 'translateY(0)';
|
card.style.transform = 'translateY(0)';
|
||||||
card.style.opacity = '1';
|
card.style.opacity = '1';
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
card.style.transition = '';
|
card.style.transition = '';
|
||||||
card.style.transform = '';
|
card.style.transform = '';
|
||||||
card.style.opacity = '';
|
card.style.opacity = '';
|
||||||
}, 310);
|
}, 310);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||||
function insertCardInSidebar(card, event) {
|
function insertCardInSidebar(card, event) {
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||||
let inserted = false;
|
let inserted = false;
|
||||||
for (const currentCard of existingCards) {
|
for (const currentCard of existingCards) {
|
||||||
const rect = currentCard.getBoundingClientRect();
|
const rect = currentCard.getBoundingClientRect();
|
||||||
const midY = rect.top + rect.height / 2;
|
const midY = rect.top + rect.height / 2;
|
||||||
if (event.clientY < midY) {
|
if (event.clientY < midY) {
|
||||||
sidebar.insertBefore(card, currentCard);
|
sidebar.insertBefore(card, currentCard);
|
||||||
inserted = true;
|
inserted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!inserted) {
|
if (!inserted) {
|
||||||
sidebar.appendChild(card);
|
sidebar.appendChild(card);
|
||||||
|
}
|
||||||
|
// Ensure card fills the sidebar.
|
||||||
|
card.style.width = '100%';
|
||||||
|
animateVerticalSlide(card);
|
||||||
}
|
}
|
||||||
// Ensure card fills the sidebar.
|
|
||||||
card.style.width = '100%';
|
// Internal helper: save the current sidebar card order to localStorage.
|
||||||
animateVerticalSlide(card);
|
function saveSidebarOrder() {
|
||||||
}
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
// 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 cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
const order = Array.from(cards).map(card => card.id);
|
const order = Array.from(cards).map(card => card.id);
|
||||||
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||||
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
function moveSidebarCardsToTop() {
|
||||||
function moveSidebarCardsToTop() {
|
if (window.innerWidth < 1205) {
|
||||||
if (window.innerWidth < 1205) {
|
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||||
if (orig) {
|
if (orig) {
|
||||||
orig.appendChild(card);
|
orig.appendChild(card);
|
||||||
animateVerticalSlide(card);
|
animateVerticalSlide(card);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateSidebarVisibility();
|
updateSidebarVisibility();
|
||||||
updateTopZoneLayout();
|
updateTopZoneLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
window.addEventListener('resize', function () {
|
||||||
window.addEventListener('resize', function () {
|
if (window.innerWidth < 1205) {
|
||||||
if (window.innerWidth < 1205) {
|
|
||||||
moveSidebarCardsToTop();
|
moveSidebarCardsToTop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||||
function ensureTopZonePlaceholder() {
|
function ensureTopZonePlaceholder() {
|
||||||
const topZone = document.getElementById('uploadFolderRow');
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
if (!topZone) return;
|
if (!topZone) return;
|
||||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||||
let placeholder = topZone.querySelector('.placeholder');
|
let placeholder = topZone.querySelector('.placeholder');
|
||||||
if (!placeholder) {
|
if (!placeholder) {
|
||||||
placeholder = document.createElement('div');
|
placeholder = document.createElement('div');
|
||||||
placeholder.className = 'placeholder';
|
placeholder.className = 'placeholder';
|
||||||
placeholder.style.visibility = 'hidden';
|
placeholder.style.visibility = 'hidden';
|
||||||
placeholder.style.display = 'block';
|
placeholder.style.display = 'block';
|
||||||
placeholder.style.width = '100%';
|
placeholder.style.width = '100%';
|
||||||
placeholder.style.height = '375px';
|
placeholder.style.height = '375px';
|
||||||
topZone.appendChild(placeholder);
|
topZone.appendChild(placeholder);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const placeholder = topZone.querySelector('.placeholder');
|
const placeholder = topZone.querySelector('.placeholder');
|
||||||
if (placeholder) placeholder.remove();
|
if (placeholder) placeholder.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE ---
|
||||||
// This sets up all drag-and-drop event listeners for cards.
|
|
||||||
export function initDragAndDrop() {
|
// Show header drop zone and add a "drag-active" class so that the pseudo-element appears.
|
||||||
function run() {
|
function showHeaderDropZone() {
|
||||||
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
|
if (headerDropArea) {
|
||||||
|
headerDropArea.style.display = 'inline-flex';
|
||||||
|
headerDropArea.classList.add('drag-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide header drop zone by removing the "drag-active" class.
|
||||||
|
// If a header icon is present (i.e. a card was dropped), the drop zone remains visible without the dashed border.
|
||||||
|
function hideHeaderDropZone() {
|
||||||
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
|
if (headerDropArea) {
|
||||||
|
headerDropArea.classList.remove('drag-active');
|
||||||
|
if (headerDropArea.children.length === 0) {
|
||||||
|
headerDropArea.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === NEW FUNCTION: Insert card into header drop zone as a material icon ===
|
||||||
|
function insertCardInHeader(card, event) {
|
||||||
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
|
if (!headerDropArea) return;
|
||||||
|
|
||||||
|
// For folder management and upload cards, preserve the original by moving it to a hidden container.
|
||||||
|
if (card.id === 'folderManagementCard' || card.id === 'uploadCard') {
|
||||||
|
let hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||||
|
if (!hiddenContainer) {
|
||||||
|
hiddenContainer = document.createElement('div');
|
||||||
|
hiddenContainer.id = 'hiddenCardsContainer';
|
||||||
|
hiddenContainer.style.display = 'none';
|
||||||
|
document.body.appendChild(hiddenContainer);
|
||||||
|
}
|
||||||
|
// Move the original card to the hidden container if it's not already there.
|
||||||
|
if (card.parentNode.id !== 'hiddenCardsContainer') {
|
||||||
|
hiddenContainer.appendChild(card);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For other cards, simply remove from current container.
|
||||||
|
if (card.parentNode) {
|
||||||
|
card.parentNode.removeChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the header icon button.
|
||||||
|
const iconButton = document.createElement('button');
|
||||||
|
iconButton.className = 'header-card-icon';
|
||||||
|
// Remove default button styling.
|
||||||
|
iconButton.style.border = 'none';
|
||||||
|
iconButton.style.background = 'none';
|
||||||
|
iconButton.style.outline = 'none';
|
||||||
|
iconButton.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
// Choose an icon based on the card type with 24px size.
|
||||||
|
if (card.id === 'uploadCard') {
|
||||||
|
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>';
|
||||||
|
} else if (card.id === 'folderManagementCard') {
|
||||||
|
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">folder</i>';
|
||||||
|
} else {
|
||||||
|
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">insert_drive_file</i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a reference to the card in the icon button.
|
||||||
|
iconButton.cardElement = card;
|
||||||
|
// Associate this icon with the card for future removal.
|
||||||
|
card.headerIconButton = iconButton;
|
||||||
|
|
||||||
|
let modal = null;
|
||||||
|
let isLocked = false;
|
||||||
|
let hoverActive = false;
|
||||||
|
|
||||||
|
// showModal: When triggered, ensure the card is attached to the modal.
|
||||||
|
function showModal() {
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.className = 'header-card-modal';
|
||||||
|
modal.style.position = 'fixed';
|
||||||
|
modal.style.top = '80px';
|
||||||
|
modal.style.right = '80px';
|
||||||
|
modal.style.zIndex = '11000';
|
||||||
|
// Render the modal but initially keep it hidden.
|
||||||
|
modal.style.display = 'block';
|
||||||
|
modal.style.visibility = 'hidden';
|
||||||
|
modal.style.opacity = '0';
|
||||||
|
modal.style.background = 'none';
|
||||||
|
modal.style.border = 'none';
|
||||||
|
modal.style.padding = '0';
|
||||||
|
modal.style.boxShadow = 'none';
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
// Attach modal hover events.
|
||||||
|
modal.addEventListener('mouseover', handleMouseOver);
|
||||||
|
modal.addEventListener('mouseout', handleMouseOut);
|
||||||
|
iconButton.modalInstance = modal;
|
||||||
|
}
|
||||||
|
// If the card isn't already in the modal, remove it from the hidden container and attach it.
|
||||||
|
if (!modal.contains(card)) {
|
||||||
|
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||||
|
if (hiddenContainer && hiddenContainer.contains(card)) {
|
||||||
|
hiddenContainer.removeChild(card);
|
||||||
|
}
|
||||||
|
modal.appendChild(card);
|
||||||
|
}
|
||||||
|
// Reveal the modal.
|
||||||
|
modal.style.visibility = 'visible';
|
||||||
|
modal.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// hideModal: Hide the modal and return the card to the hidden container.
|
||||||
|
function hideModal() {
|
||||||
|
if (modal && !isLocked && !hoverActive) {
|
||||||
|
modal.style.visibility = 'hidden';
|
||||||
|
modal.style.opacity = '0';
|
||||||
|
// Return the card to the hidden container.
|
||||||
|
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||||
|
if (hiddenContainer && modal.contains(card)) {
|
||||||
|
hiddenContainer.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseOver() {
|
||||||
|
hoverActive = true;
|
||||||
|
showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseOut() {
|
||||||
|
hoverActive = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hoverActive && !isLocked) {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach hover events to the icon.
|
||||||
|
iconButton.addEventListener('mouseover', handleMouseOver);
|
||||||
|
iconButton.addEventListener('mouseout', handleMouseOut);
|
||||||
|
|
||||||
|
// Toggle the locked state on click so the modal stays open.
|
||||||
|
iconButton.addEventListener('click', (e) => {
|
||||||
|
isLocked = !isLocked;
|
||||||
|
if (isLocked) {
|
||||||
|
showModal();
|
||||||
|
} else {
|
||||||
|
hideModal();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append the header icon button into the header drop zone.
|
||||||
|
headerDropArea.appendChild(iconButton);
|
||||||
|
// Save the updated header order.
|
||||||
|
saveHeaderOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Main Drag and Drop Initialization ===
|
||||||
|
export function initDragAndDrop() {
|
||||||
|
function run() {
|
||||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
draggableCards.forEach(card => {
|
draggableCards.forEach(card => {
|
||||||
if (!card.dataset.originalContainerId) {
|
if (!card.dataset.originalContainerId) {
|
||||||
card.dataset.originalContainerId = card.parentNode.id;
|
card.dataset.originalContainerId = card.parentNode.id;
|
||||||
}
|
}
|
||||||
const header = card.querySelector('.card-header');
|
const header = card.querySelector('.card-header');
|
||||||
if (header) {
|
if (header) {
|
||||||
header.classList.add('drag-header');
|
header.classList.add('drag-header');
|
||||||
}
|
}
|
||||||
|
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragTimer = null;
|
let dragTimer = null;
|
||||||
let offsetX = 0, offsetY = 0;
|
let offsetX = 0, offsetY = 0;
|
||||||
let initialLeft, initialTop;
|
let initialLeft, initialTop;
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
header.addEventListener('mousedown', function (e) {
|
header.addEventListener('mousedown', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const card = this.closest('.card');
|
const card = this.closest('.card');
|
||||||
// Capture the card's initial bounding rectangle once.
|
// Capture the card's initial bounding rectangle.
|
||||||
const initialRect = card.getBoundingClientRect();
|
const initialRect = card.getBoundingClientRect();
|
||||||
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
||||||
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
||||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||||
|
|
||||||
// Store the initial rect so we use it later.
|
// Store the initial rect so we use it later.
|
||||||
dragTimer = setTimeout(() => {
|
dragTimer = setTimeout(() => {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
card.classList.add('dragging');
|
card.classList.add('dragging');
|
||||||
card.style.pointerEvents = 'none';
|
card.style.pointerEvents = 'none';
|
||||||
addTopZoneHighlight();
|
addTopZoneHighlight();
|
||||||
|
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.add('active');
|
sidebar.classList.add('active');
|
||||||
sidebar.style.display = 'block';
|
sidebar.style.display = 'block';
|
||||||
sidebar.classList.add('highlight');
|
sidebar.classList.add('highlight');
|
||||||
sidebar.style.height = '800px';
|
sidebar.style.height = '800px';
|
||||||
}
|
|
||||||
|
|
||||||
// Use the stored initialRect rather than recalculating.
|
|
||||||
initialLeft = initialRect.left + window.pageXOffset;
|
|
||||||
initialTop = initialRect.top + window.pageYOffset;
|
|
||||||
offsetX = e.pageX - initialLeft;
|
|
||||||
offsetY = e.pageY - initialTop;
|
|
||||||
|
|
||||||
// Append card to body and fix its dimensions to prevent shrinking.
|
|
||||||
document.body.appendChild(card);
|
|
||||||
card.style.position = 'absolute';
|
|
||||||
card.style.left = initialLeft + 'px';
|
|
||||||
card.style.top = initialTop + 'px';
|
|
||||||
card.style.width = initialRect.width + 'px';
|
|
||||||
card.style.height = initialRect.height + 'px';
|
|
||||||
card.style.minWidth = initialRect.width + 'px';
|
|
||||||
card.style.flexShrink = '0';
|
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show header drop zone while dragging.
|
||||||
|
showHeaderDropZone();
|
||||||
|
|
||||||
|
// Use the stored initialRect.
|
||||||
|
initialLeft = initialRect.left + window.pageXOffset;
|
||||||
|
initialTop = initialRect.top + window.pageYOffset;
|
||||||
|
offsetX = e.pageX - initialLeft;
|
||||||
|
offsetY = e.pageY - initialTop;
|
||||||
|
|
||||||
|
// Remove any associated header icon if present.
|
||||||
|
if (card.headerIconButton) {
|
||||||
|
if (card.headerIconButton.parentNode) {
|
||||||
|
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||||
|
}
|
||||||
|
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||||
|
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||||
|
}
|
||||||
|
card.headerIconButton = null;
|
||||||
|
saveHeaderOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append card to body and fix its dimensions.
|
||||||
|
document.body.appendChild(card);
|
||||||
|
card.style.position = 'absolute';
|
||||||
|
card.style.left = initialLeft + 'px';
|
||||||
|
card.style.top = initialTop + 'px';
|
||||||
|
card.style.width = initialRect.width + 'px';
|
||||||
|
card.style.height = initialRect.height + 'px';
|
||||||
|
card.style.minWidth = initialRect.width + 'px';
|
||||||
|
card.style.flexShrink = '0';
|
||||||
|
card.style.zIndex = '10000';
|
||||||
|
}, 500);
|
||||||
});
|
});
|
||||||
|
header.addEventListener('mouseup', function () {
|
||||||
document.addEventListener('mouseup', function (e) {
|
clearTimeout(dragTimer);
|
||||||
if (isDragging) {
|
});
|
||||||
isDragging = false;
|
}
|
||||||
card.style.pointerEvents = '';
|
|
||||||
card.classList.remove('dragging');
|
document.addEventListener('mousemove', function (e) {
|
||||||
removeTopZoneHighlight();
|
if (isDragging) {
|
||||||
|
card.style.left = (e.pageX - offsetX) + 'px';
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
card.style.top = (e.pageY - offsetY) + 'px';
|
||||||
if (sidebar) {
|
}
|
||||||
sidebar.classList.remove('highlight');
|
});
|
||||||
sidebar.style.height = '';
|
|
||||||
}
|
document.addEventListener('mouseup', function (e) {
|
||||||
|
if (isDragging) {
|
||||||
let droppedInSidebar = false;
|
isDragging = false;
|
||||||
let droppedInTop = false;
|
card.style.pointerEvents = '';
|
||||||
|
card.classList.remove('dragging');
|
||||||
// Check if dropped in sidebar drop zone.
|
removeTopZoneHighlight();
|
||||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
|
||||||
if (sidebarElem) {
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
const rect = sidebarElem.getBoundingClientRect();
|
if (sidebar) {
|
||||||
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
sidebar.classList.remove('highlight');
|
||||||
if (
|
sidebar.style.height = '';
|
||||||
e.clientX >= rect.left &&
|
}
|
||||||
e.clientX <= rect.right &&
|
|
||||||
e.clientY >= rect.top &&
|
// Remove any existing header icon if present.
|
||||||
e.clientY <= dropZoneBottom
|
if (card.headerIconButton) {
|
||||||
) {
|
if (card.headerIconButton.parentNode) {
|
||||||
insertCardInSidebar(card, e);
|
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||||
droppedInSidebar = true;
|
}
|
||||||
}
|
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||||
}
|
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||||
// If not dropped in sidebar, check the top drop zone.
|
}
|
||||||
const topRow = document.getElementById('uploadFolderRow');
|
card.headerIconButton = null;
|
||||||
if (!droppedInSidebar && topRow) {
|
saveHeaderOrder();
|
||||||
const rect = topRow.getBoundingClientRect();
|
}
|
||||||
if (
|
|
||||||
e.clientX >= rect.left &&
|
let droppedInSidebar = false;
|
||||||
e.clientX <= rect.right &&
|
let droppedInTop = false;
|
||||||
e.clientY >= rect.top &&
|
let droppedInHeader = false;
|
||||||
e.clientY <= rect.bottom
|
|
||||||
) {
|
// Check if dropped in sidebar drop zone.
|
||||||
let container;
|
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||||
if (card.id === 'uploadCard') {
|
if (sidebarElem) {
|
||||||
container = document.getElementById('leftCol');
|
const rect = sidebarElem.getBoundingClientRect();
|
||||||
} else if (card.id === 'folderManagementCard') {
|
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||||
container = document.getElementById('rightCol');
|
if (
|
||||||
}
|
e.clientX >= rect.left &&
|
||||||
if (container) {
|
e.clientX <= rect.right &&
|
||||||
ensureTopZonePlaceholder();
|
e.clientY >= rect.top &&
|
||||||
updateTopZoneLayout();
|
e.clientY <= dropZoneBottom
|
||||||
container.appendChild(card);
|
) {
|
||||||
droppedInTop = true;
|
insertCardInSidebar(card, e);
|
||||||
// Use computed style to determine container's width.
|
droppedInSidebar = true;
|
||||||
const containerWidth = parseFloat(window.getComputedStyle(container).width);
|
}
|
||||||
card.style.width = "363px";
|
}
|
||||||
// Animate the card sliding in.
|
// Check the top drop zone.
|
||||||
animateVerticalSlide(card);
|
const topRow = document.getElementById('uploadFolderRow');
|
||||||
// After animation completes, clear the inline width.
|
if (!droppedInSidebar && topRow) {
|
||||||
setTimeout(() => {
|
const rect = topRow.getBoundingClientRect();
|
||||||
card.style.removeProperty('width');
|
if (
|
||||||
}, 210);
|
e.clientX >= rect.left &&
|
||||||
}
|
e.clientX <= rect.right &&
|
||||||
}
|
e.clientY >= rect.top &&
|
||||||
}
|
e.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
// If dropped in neither area, return card to its original container.
|
let container;
|
||||||
if (!droppedInSidebar && !droppedInTop) {
|
if (card.id === 'uploadCard') {
|
||||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
container = document.getElementById('leftCol');
|
||||||
if (orig) {
|
} else if (card.id === 'folderManagementCard') {
|
||||||
orig.appendChild(card);
|
container = document.getElementById('rightCol');
|
||||||
card.style.removeProperty('width');
|
}
|
||||||
}
|
if (container) {
|
||||||
}
|
ensureTopZonePlaceholder();
|
||||||
|
|
||||||
// Clear inline styles from dragging.
|
|
||||||
[
|
|
||||||
'position',
|
|
||||||
'left',
|
|
||||||
'top',
|
|
||||||
'z-index',
|
|
||||||
'height',
|
|
||||||
'min-width',
|
|
||||||
'flex-shrink',
|
|
||||||
'transition',
|
|
||||||
'transform',
|
|
||||||
'opacity'
|
|
||||||
].forEach(prop => card.style.removeProperty(prop));
|
|
||||||
|
|
||||||
// For sidebar drops, force width to 100%.
|
|
||||||
if (droppedInSidebar) {
|
|
||||||
card.style.width = '100%';
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTopZoneLayout();
|
updateTopZoneLayout();
|
||||||
updateSidebarVisibility();
|
container.appendChild(card);
|
||||||
|
droppedInTop = true;
|
||||||
|
// Set a fixed width during animation.
|
||||||
|
card.style.width = "363px";
|
||||||
|
animateVerticalSlide(card);
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.removeProperty('width');
|
||||||
|
}, 210);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
// Check the header drop zone.
|
||||||
|
const headerDropArea = document.getElementById('headerDropArea');
|
||||||
|
if (!droppedInSidebar && !droppedInTop && headerDropArea) {
|
||||||
|
const rect = headerDropArea.getBoundingClientRect();
|
||||||
|
if (
|
||||||
|
e.clientX >= rect.left &&
|
||||||
|
e.clientX <= rect.right &&
|
||||||
|
e.clientY >= rect.top &&
|
||||||
|
e.clientY <= rect.bottom
|
||||||
|
) {
|
||||||
|
insertCardInHeader(card, e);
|
||||||
|
droppedInHeader = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If card was not dropped in any zone, return it to its original container.
|
||||||
|
if (!droppedInSidebar && !droppedInTop && !droppedInHeader) {
|
||||||
|
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||||
|
if (orig) {
|
||||||
|
orig.appendChild(card);
|
||||||
|
card.style.removeProperty('width');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear inline drag-related styles.
|
||||||
|
[
|
||||||
|
'position',
|
||||||
|
'left',
|
||||||
|
'top',
|
||||||
|
'z-index',
|
||||||
|
'height',
|
||||||
|
'min-width',
|
||||||
|
'flex-shrink',
|
||||||
|
'transition',
|
||||||
|
'transform',
|
||||||
|
'opacity'
|
||||||
|
].forEach(prop => card.style.removeProperty(prop));
|
||||||
|
|
||||||
|
// For sidebar drops, force width to 100%.
|
||||||
|
if (droppedInSidebar) {
|
||||||
|
card.style.width = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTopZoneLayout();
|
||||||
|
updateSidebarVisibility();
|
||||||
|
|
||||||
|
// Hide header drop zone if no icon is present.
|
||||||
|
hideHeaderDropZone();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', run);
|
document.addEventListener('DOMContentLoaded', run);
|
||||||
} else {
|
} else {
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
echo json_encode(["error" => "Unauthorized"]);
|
echo json_encode(["error" => "Unauthorized"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
105
fileManager.js
@@ -263,7 +263,7 @@ function previewFile(fileUrl, fileName) {
|
|||||||
embed.style.height = "80vh";
|
embed.style.height = "80vh";
|
||||||
embed.style.border = "none";
|
embed.style.border = "none";
|
||||||
container.appendChild(embed);
|
container.appendChild(embed);
|
||||||
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
|
} else if (/\.(mp4|mkv|webm|mov|ogv)$/i.test(fileName)) {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.src = fileUrl;
|
video.src = fileUrl;
|
||||||
video.controls = true;
|
video.controls = true;
|
||||||
@@ -365,75 +365,74 @@ export function loadFileList(folderParam) {
|
|||||||
//
|
//
|
||||||
function fileDragStartHandler(event) {
|
function fileDragStartHandler(event) {
|
||||||
const row = event.currentTarget;
|
const row = event.currentTarget;
|
||||||
|
let fileNames = [];
|
||||||
|
|
||||||
// Check if multiple file checkboxes are selected.
|
// Check if multiple file checkboxes are selected.
|
||||||
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked");
|
||||||
let fileNames = [];
|
|
||||||
if (selectedCheckboxes.length > 1) {
|
if (selectedCheckboxes.length > 1) {
|
||||||
// Gather file names from all selected rows.
|
|
||||||
selectedCheckboxes.forEach(chk => {
|
selectedCheckboxes.forEach(chk => {
|
||||||
const parentRow = chk.closest("tr");
|
const parentRow = chk.closest("tr");
|
||||||
if (parentRow) {
|
if (parentRow) {
|
||||||
const cell = parentRow.querySelector("td:nth-child(2)");
|
const cell = parentRow.querySelector("td:nth-child(2)");
|
||||||
if (cell) fileNames.push(cell.textContent.trim());
|
if (cell) {
|
||||||
|
let rawName = cell.textContent.trim();
|
||||||
|
// Attempt to get the tag text from a container that holds the tags.
|
||||||
|
const tagContainer = cell.querySelector(".tag-badges");
|
||||||
|
if (tagContainer) {
|
||||||
|
const tagText = tagContainer.innerText.trim();
|
||||||
|
if (rawName.endsWith(tagText)) {
|
||||||
|
rawName = rawName.slice(0, -tagText.length).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileNames.push(rawName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Only one file is selected (or none), so get file name from the current row.
|
|
||||||
const fileNameCell = row.querySelector("td:nth-child(2)");
|
const fileNameCell = row.querySelector("td:nth-child(2)");
|
||||||
if (fileNameCell) {
|
if (fileNameCell) {
|
||||||
fileNames.push(fileNameCell.textContent.trim());
|
let rawName = fileNameCell.textContent.trim();
|
||||||
|
const tagContainer = fileNameCell.querySelector(".tag-badges");
|
||||||
|
if (tagContainer) {
|
||||||
|
const tagText = tagContainer.innerText.trim();
|
||||||
|
if (rawName.endsWith(tagText)) {
|
||||||
|
rawName = rawName.slice(0, -tagText.length).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileNames.push(rawName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileNames.length === 0) return;
|
if (fileNames.length === 0) return;
|
||||||
const dragData = {
|
|
||||||
files: fileNames, // use an array of file names
|
// For a single file, send fileName; for multiple, send an array.
|
||||||
sourceFolder: window.currentFolder || "root"
|
const dragData = fileNames.length === 1
|
||||||
};
|
? { fileName: fileNames[0], sourceFolder: window.currentFolder || "root" }
|
||||||
|
: { files: fileNames, sourceFolder: window.currentFolder || "root" };
|
||||||
|
|
||||||
event.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
event.dataTransfer.setData("application/json", JSON.stringify(dragData));
|
||||||
|
|
||||||
// (Keep your custom drag image code here.)
|
// Create a custom drag image.
|
||||||
let dragImage;
|
let dragImage = document.createElement("div");
|
||||||
if (fileNames.length > 1) {
|
dragImage.style.display = "inline-flex";
|
||||||
dragImage = document.createElement("div");
|
dragImage.style.width = "auto";
|
||||||
dragImage.style.display = "inline-flex";
|
dragImage.style.maxWidth = "fit-content";
|
||||||
dragImage.style.width = "auto";
|
dragImage.style.padding = "6px 10px";
|
||||||
dragImage.style.maxWidth = "fit-content";
|
dragImage.style.backgroundColor = "#333";
|
||||||
dragImage.style.padding = "6px 10px";
|
dragImage.style.color = "#fff";
|
||||||
dragImage.style.backgroundColor = "#333";
|
dragImage.style.border = "1px solid #555";
|
||||||
dragImage.style.color = "#fff";
|
dragImage.style.borderRadius = "4px";
|
||||||
dragImage.style.border = "1px solid #555";
|
dragImage.style.alignItems = "center";
|
||||||
dragImage.style.borderRadius = "4px";
|
dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)";
|
||||||
dragImage.style.alignItems = "center";
|
const icon = document.createElement("span");
|
||||||
dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)";
|
icon.className = "material-icons";
|
||||||
const icon = document.createElement("span");
|
icon.textContent = "insert_drive_file";
|
||||||
icon.className = "material-icons";
|
icon.style.marginRight = "4px";
|
||||||
icon.textContent = "insert_drive_file";
|
const label = document.createElement("span");
|
||||||
icon.style.marginRight = "4px";
|
label.textContent = fileNames.length === 1 ? fileNames[0] : fileNames.length + " files";
|
||||||
const countSpan = document.createElement("span");
|
dragImage.appendChild(icon);
|
||||||
countSpan.textContent = fileNames.length + " files";
|
dragImage.appendChild(label);
|
||||||
dragImage.appendChild(icon);
|
|
||||||
dragImage.appendChild(countSpan);
|
|
||||||
} else {
|
|
||||||
dragImage = document.createElement("div");
|
|
||||||
dragImage.style.display = "inline-flex";
|
|
||||||
dragImage.style.width = "auto";
|
|
||||||
dragImage.style.maxWidth = "fit-content";
|
|
||||||
dragImage.style.padding = "6px 10px";
|
|
||||||
dragImage.style.backgroundColor = "#333";
|
|
||||||
dragImage.style.color = "#fff";
|
|
||||||
dragImage.style.border = "1px solid #555";
|
|
||||||
dragImage.style.borderRadius = "4px";
|
|
||||||
dragImage.style.alignItems = "center";
|
|
||||||
dragImage.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.3)";
|
|
||||||
const icon = document.createElement("span");
|
|
||||||
icon.className = "material-icons";
|
|
||||||
icon.textContent = "insert_drive_file";
|
|
||||||
icon.style.marginRight = "4px";
|
|
||||||
const nameSpan = document.createElement("span");
|
|
||||||
nameSpan.textContent = fileNames[0];
|
|
||||||
dragImage.appendChild(icon);
|
|
||||||
dragImage.appendChild(nameSpan);
|
|
||||||
}
|
|
||||||
document.body.appendChild(dragImage);
|
document.body.appendChild(dragImage);
|
||||||
event.dataTransfer.setDragImage(dragImage, 5, 5);
|
event.dataTransfer.setDragImage(dragImage, 5, 5);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -95,6 +95,9 @@
|
|||||||
<h1>FileRise</h1>
|
<h1>FileRise</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<!-- Your header drop zone -->
|
||||||
|
<div id="headerDropArea" class="header-drop-zone"></div>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
<button id="logoutBtn" title="Logout">
|
<button id="logoutBtn" title="Logout">
|
||||||
<i class="material-icons">exit_to_app</i>
|
<i class="material-icons">exit_to_app</i>
|
||||||
@@ -123,7 +126,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="addUserBtn" title="Add User" style="display: none;">
|
<button id="addUserBtn" title="Add User" style="display: none;">
|
||||||
<i class="material-icons">person_add</i>
|
<i class="material-icons">person_add</i>
|
||||||
</button>
|
</button>
|
||||||
<button id="removeUserBtn" title="Remove User" style="display: none;">
|
<button id="removeUserBtn" title="Remove User" style="display: none;">
|
||||||
@@ -132,10 +135,12 @@
|
|||||||
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Custom Toast Container -->
|
<!-- Custom Toast Container -->
|
||||||
<div id="customToast"></div>
|
<div id="customToast"></div>
|
||||||
|
<div id="hiddenCardsContainer" style="display:none;"></div>
|
||||||
|
|
||||||
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
@@ -390,7 +395,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
197
main.js
@@ -17,12 +17,17 @@ import { loadFolderTree } from './folderManager.js';
|
|||||||
import { initUpload } from './upload.js';
|
import { initUpload } from './upload.js';
|
||||||
import { initAuth, checkAuthentication } from './auth.js';
|
import { initAuth, checkAuthentication } from './auth.js';
|
||||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||||
import { initDragAndDrop, loadSidebarOrder } from './dragAndDrop.js'
|
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js';
|
||||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||||
|
|
||||||
function loadCsrfToken() {
|
function loadCsrfTokenWithRetry(retries = 3, delay = 1000) {
|
||||||
fetch('token.php', { credentials: 'include' })
|
return fetch('token.php', { credentials: 'include' })
|
||||||
.then(response => response.json())
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Token fetch failed with status: " + response.status);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Set global variables.
|
// Set global variables.
|
||||||
window.csrfToken = data.csrf_token;
|
window.csrfToken = data.csrf_token;
|
||||||
@@ -45,11 +50,19 @@ function loadCsrfToken() {
|
|||||||
document.head.appendChild(metaShare);
|
document.head.appendChild(metaShare);
|
||||||
}
|
}
|
||||||
metaShare.setAttribute('content', data.share_url);
|
metaShare.setAttribute('content', data.share_url);
|
||||||
})
|
|
||||||
.catch(error => console.error("Error loading CSRF token and share URL:", error));
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", loadCsrfToken);
|
return data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (retries > 0) {
|
||||||
|
console.warn(`CSRF token load failed. Retrying in ${delay}ms... (${retries} retries left)`, error);
|
||||||
|
return new Promise(resolve => setTimeout(resolve, delay))
|
||||||
|
.then(() => loadCsrfTokenWithRetry(retries - 1, delay * 2));
|
||||||
|
}
|
||||||
|
console.error("Failed to load CSRF token after retries.", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Expose functions for inline handlers.
|
// Expose functions for inline handlers.
|
||||||
window.sendRequest = sendRequest;
|
window.sendRequest = sendRequest;
|
||||||
@@ -63,94 +76,104 @@ window.renameFile = renameFile;
|
|||||||
window.currentFolder = "root";
|
window.currentFolder = "root";
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Call initAuth synchronously.
|
// First, load the CSRF token (with retry).
|
||||||
initAuth();
|
loadCsrfTokenWithRetry().then(() => {
|
||||||
|
// Once CSRF token is loaded, initialize authentication.
|
||||||
|
initAuth();
|
||||||
|
|
||||||
const newPasswordInput = document.getElementById("newPassword");
|
// Continue with initializations that rely on a valid CSRF token:
|
||||||
if (newPasswordInput) {
|
checkAuthentication().then(authenticated => {
|
||||||
newPasswordInput.addEventListener("input", function() {
|
if (authenticated) {
|
||||||
console.log("newPassword input event:", this.value);
|
window.currentFolder = "root";
|
||||||
});
|
initTagSearch();
|
||||||
} else {
|
loadFileList(window.currentFolder);
|
||||||
console.error("newPassword input not found!");
|
initDragAndDrop();
|
||||||
}
|
loadSidebarOrder();
|
||||||
// --- Dark Mode Persistence ---
|
loadHeaderOrder();
|
||||||
const darkModeToggle = document.getElementById("darkModeToggle");
|
initFileActions();
|
||||||
const storedDarkMode = localStorage.getItem("darkMode");
|
initUpload();
|
||||||
|
loadFolderTree();
|
||||||
|
setupTrashRestoreDelete();
|
||||||
|
|
||||||
if (storedDarkMode === "true") {
|
const helpBtn = document.getElementById("folderHelpBtn");
|
||||||
document.body.classList.add("dark-mode");
|
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||||
} else if (storedDarkMode === "false") {
|
helpBtn.addEventListener("click", function () {
|
||||||
document.body.classList.remove("dark-mode");
|
// Toggle display of the tooltip.
|
||||||
} else {
|
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
||||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
helpTooltip.style.display = "block";
|
||||||
document.body.classList.add("dark-mode");
|
} else {
|
||||||
} else {
|
helpTooltip.style.display = "none";
|
||||||
document.body.classList.remove("dark-mode");
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (darkModeToggle) {
|
|
||||||
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
|
||||||
? "Light Mode"
|
|
||||||
: "Dark Mode";
|
|
||||||
|
|
||||||
darkModeToggle.addEventListener("click", function () {
|
|
||||||
if (document.body.classList.contains("dark-mode")) {
|
|
||||||
document.body.classList.remove("dark-mode");
|
|
||||||
localStorage.setItem("darkMode", "false");
|
|
||||||
darkModeToggle.textContent = "Dark Mode";
|
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.add("dark-mode");
|
console.warn("User not authenticated. Data loading deferred.");
|
||||||
localStorage.setItem("darkMode", "true");
|
|
||||||
darkModeToggle.textContent = "Light Mode";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
// Other DOM initialization that can happen after CSRF is ready.
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
const newPasswordInput = document.getElementById("newPassword");
|
||||||
if (event.matches) {
|
if (newPasswordInput) {
|
||||||
document.body.classList.add("dark-mode");
|
newPasswordInput.addEventListener("input", function() {
|
||||||
if (darkModeToggle) darkModeToggle.textContent = "Light Mode";
|
console.log("newPassword input event:", this.value);
|
||||||
} else {
|
|
||||||
document.body.classList.remove("dark-mode");
|
|
||||||
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// --- End Dark Mode Persistence ---
|
|
||||||
|
|
||||||
const message = sessionStorage.getItem("welcomeMessage");
|
|
||||||
if (message) {
|
|
||||||
showToast(message);
|
|
||||||
sessionStorage.removeItem("welcomeMessage");
|
|
||||||
}
|
|
||||||
|
|
||||||
checkAuthentication().then(authenticated => {
|
|
||||||
if (authenticated) {
|
|
||||||
window.currentFolder = "root";
|
|
||||||
initTagSearch();
|
|
||||||
loadFileList(window.currentFolder);
|
|
||||||
initDragAndDrop();
|
|
||||||
loadSidebarOrder();
|
|
||||||
initFileActions();
|
|
||||||
initUpload();
|
|
||||||
loadFolderTree();
|
|
||||||
setupTrashRestoreDelete();
|
|
||||||
const helpBtn = document.getElementById("folderHelpBtn");
|
|
||||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
|
||||||
helpBtn.addEventListener("click", function () {
|
|
||||||
// Toggle display of the tooltip.
|
|
||||||
if (helpTooltip.style.display === "none" || helpTooltip.style.display === "") {
|
|
||||||
helpTooltip.style.display = "block";
|
|
||||||
} else {
|
|
||||||
helpTooltip.style.display = "none";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn("User not authenticated. Data loading deferred.");
|
console.error("newPassword input not found!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Dark Mode Persistence ---
|
||||||
|
const darkModeToggle = document.getElementById("darkModeToggle");
|
||||||
|
const storedDarkMode = localStorage.getItem("darkMode");
|
||||||
|
|
||||||
|
if (storedDarkMode === "true") {
|
||||||
|
document.body.classList.add("dark-mode");
|
||||||
|
} else if (storedDarkMode === "false") {
|
||||||
|
document.body.classList.remove("dark-mode");
|
||||||
|
} else {
|
||||||
|
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
document.body.classList.add("dark-mode");
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("dark-mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (darkModeToggle) {
|
||||||
|
darkModeToggle.textContent = document.body.classList.contains("dark-mode")
|
||||||
|
? "Light Mode"
|
||||||
|
: "Dark Mode";
|
||||||
|
|
||||||
|
darkModeToggle.addEventListener("click", function () {
|
||||||
|
if (document.body.classList.contains("dark-mode")) {
|
||||||
|
document.body.classList.remove("dark-mode");
|
||||||
|
localStorage.setItem("darkMode", "false");
|
||||||
|
darkModeToggle.textContent = "Dark Mode";
|
||||||
|
} else {
|
||||||
|
document.body.classList.add("dark-mode");
|
||||||
|
localStorage.setItem("darkMode", "true");
|
||||||
|
darkModeToggle.textContent = "Light Mode";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem("darkMode") === null && window.matchMedia) {
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => {
|
||||||
|
if (event.matches) {
|
||||||
|
document.body.classList.add("dark-mode");
|
||||||
|
if (darkModeToggle) darkModeToggle.textContent = "Light Mode";
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove("dark-mode");
|
||||||
|
if (darkModeToggle) darkModeToggle.textContent = "Dark Mode";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// --- End Dark Mode Persistence ---
|
||||||
|
|
||||||
|
const message = sessionStorage.getItem("welcomeMessage");
|
||||||
|
if (message) {
|
||||||
|
showToast(message);
|
||||||
|
sessionStorage.removeItem("welcomeMessage");
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Initialization halted due to CSRF token load failure.", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Auto-scroll During Drag ---
|
// --- Auto-scroll During Drag ---
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ if ($receivedToken !== $_SESSION['csrf_token']) {
|
|||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 347 KiB After Width: | Height: | Size: 410 KiB |
BIN
resources/dark-header.png
Normal file
|
After Width: | Height: | Size: 499 KiB |
|
Before Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
BIN
resources/dark-sidebar.png
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
resources/dark-totp-setup.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
resources/light-admin-panel.png
Normal file
|
After Width: | Height: | Size: 438 KiB |
BIN
resources/light-drag-file.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 400 KiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 4.0 MiB |
BIN
resources/light-topbar.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 502 KiB After Width: | Height: | Size: 457 KiB |
@@ -18,9 +18,8 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
// Check if the user is read-only. (Assuming that if readOnly is true, deletion is disallowed.)
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
if (isset($userPermissions['readOnly']) && $userPermissions['readOnly'] === true) {
|
||||||
|
|||||||
65
styles.css
@@ -1068,7 +1068,7 @@ body.dark-mode .custom-prev-next-btn:hover:not(:disabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#customToast.show {
|
#customToast.show {
|
||||||
opacity: 0.7;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-wrap {
|
.button-wrap {
|
||||||
@@ -2023,20 +2023,17 @@ body.dark-mode .card {
|
|||||||
z-index: 6000 !important;
|
z-index: 6000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default (light mode) for admin panel content */
|
|
||||||
.admin-panel-content {
|
.admin-panel-content {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode overrides for admin panel content */
|
|
||||||
body.dark-mode .admin-panel-content {
|
body.dark-mode .admin-panel-content {
|
||||||
background: #2c2c2c; /* dark background */
|
background: #2c2c2c;
|
||||||
color: #e0e0e0; /* light text */
|
color: #e0e0e0;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optionally, adjust input, label, etc. for dark mode */
|
|
||||||
body.dark-mode .admin-panel-content input,
|
body.dark-mode .admin-panel-content input,
|
||||||
body.dark-mode .admin-panel-content select,
|
body.dark-mode .admin-panel-content select,
|
||||||
body.dark-mode .admin-panel-content textarea {
|
body.dark-mode .admin-panel-content textarea {
|
||||||
@@ -2066,4 +2063,60 @@ body.dark-mode .admin-panel-content label {
|
|||||||
}
|
}
|
||||||
.spinning {
|
.spinning {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rise-effect {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-modal-btn,
|
||||||
|
.collapse-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #616161;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-modal-btn:hover,
|
||||||
|
.collapse-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-modal-btn:focus,
|
||||||
|
.collapse-btn:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-drop-zone {
|
||||||
|
width: 66px;
|
||||||
|
height: 36px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-drop-zone.drag-active {
|
||||||
|
border: 2px dashed #1565C0;
|
||||||
|
background-color: #eef;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body.dark-mode .header-drop-zone.drag-active {
|
||||||
|
background-color: #333;
|
||||||
|
border: 2px dashed #555;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-drop-zone.drag-active:empty::before {
|
||||||
|
content: "Drop";
|
||||||
|
font-size: 10px;
|
||||||
|
color: #aaa;
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,9 @@ if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
|
|||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
$userPermissions = loadUserPermissions($username);
|
|
||||||
$username = $_SESSION['username'] ?? '';
|
$username = $_SESSION['username'] ?? '';
|
||||||
|
$userPermissions = loadUserPermissions($username);
|
||||||
if ($username) {
|
if ($username) {
|
||||||
$userPermissions = loadUserPermissions($username);
|
$userPermissions = loadUserPermissions($username);
|
||||||
if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) {
|
if (isset($userPermissions['disableUpload']) && $userPermissions['disableUpload'] === true) {
|
||||||
|
|||||||