From 2d02eddf3c3502938298afc58b77cd94b1a213a8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 19 Mar 2025 22:17:47 -0400 Subject: [PATCH] drag drop files into folder tree added --- fileManager.js | 347 +++++++++++++++++++++++++++++++++-------------- folderManager.js | 79 +++++++++-- main.js | 20 ++- moveFiles.php | 2 +- styles.css | 5 + 5 files changed, 331 insertions(+), 122 deletions(-) diff --git a/fileManager.js b/fileManager.js index 5258dad..7cc671e 100644 --- a/fileManager.js +++ b/fileManager.js @@ -47,7 +47,6 @@ window.createViewToggleButton = createViewToggleButton; // ----------------------------- // Helper: formatFolderName // ----------------------------- - function formatFolderName(folder) { if (folder === "root") return "(Root)"; return folder @@ -62,7 +61,6 @@ window.updateRowHighlight = updateRowHighlight; // ============================================== // FEATURE: Public File Sharing Modal // ============================================== - function openShareModal(file, folder) { const existing = document.getElementById("shareModal"); if (existing) existing.remove(); @@ -125,7 +123,6 @@ function openShareModal(file, folder) { .then(response => response.json()) .then(data => { if (data.token) { - // Get the share endpoint from the meta tag (or fallback to a global variable) let shareEndpoint = document.querySelector('meta[name="share-url"]') ? document.querySelector('meta[name="share-url"]').getAttribute('content') : (window.SHARE_URL || "share.php"); @@ -143,7 +140,7 @@ function openShareModal(file, folder) { showToast("Error generating share link."); }); }); - + document.getElementById("copyShareLinkBtn").addEventListener("click", () => { const input = document.getElementById("shareLinkInput"); input.select(); @@ -154,10 +151,7 @@ function openShareModal(file, folder) { // ============================================== // FEATURE: Enhanced Preview Modal with Navigation -// ============================================= -// This function replaces the previous preview behavior for images. -// It uses your original modal layout and, if multiple images exist, -// overlays transparent Prev/Next buttons over the image. +// ============================================== function enhancedPreviewFile(fileUrl, fileName) { let modal = document.getElementById("filePreviewModal"); if (!modal) { @@ -206,7 +200,6 @@ function enhancedPreviewFile(fileUrl, fileName) { img.style.maxHeight = "80vh"; container.appendChild(img); - // If multiple images exist, add arrow navigation. const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)); if (images.length > 1) { modal.galleryImages = images; @@ -221,10 +214,10 @@ function enhancedPreviewFile(fileUrl, fileName) { modal.galleryCurrentIndex = (modal.galleryCurrentIndex - 1 + modal.galleryImages.length) % modal.galleryImages.length; let newFile = modal.galleryImages[modal.galleryCurrentIndex]; modal.querySelector("h4").textContent = newFile.name; - img.src = ((window.currentFolder === "root") - ? "uploads/" - : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") - + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); + img.src = ((window.currentFolder === "root") + ? "uploads/" + : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") + + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); }); const nextBtn = document.createElement("button"); nextBtn.textContent = "›"; @@ -235,10 +228,10 @@ function enhancedPreviewFile(fileUrl, fileName) { modal.galleryCurrentIndex = (modal.galleryCurrentIndex + 1) % modal.galleryImages.length; let newFile = modal.galleryImages[modal.galleryCurrentIndex]; modal.querySelector("h4").textContent = newFile.name; - img.src = ((window.currentFolder === "root") - ? "uploads/" - : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") - + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); + img.src = ((window.currentFolder === "root") + ? "uploads/" + : "uploads/" + window.currentFolder.split("/").map(encodeURIComponent).join("/") + "/") + + encodeURIComponent(newFile.name) + "?t=" + new Date().getTime(); }); container.appendChild(prevBtn); container.appendChild(nextBtn); @@ -282,12 +275,9 @@ export function loadFileList(folderParam) { return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) .then(response => { - // Check if the session has expired. if (response.status === 401) { showToast("Session expired. Please log in again."); - // Redirect to logout.php to clear the session; this can trigger a login process. window.location.href = "logout.php"; - // Throw error to stop further processing. throw new Error("Unauthorized"); } return response.json(); @@ -318,7 +308,6 @@ export function loadFileList(folderParam) { }) .catch(error => { console.error("Error loading file list:", error); - // Only update the container text if error is not due to an unauthorized response. if (error.message !== "Unauthorized") { fileListContainer.textContent = "Error loading files."; } @@ -329,10 +318,94 @@ export function loadFileList(folderParam) { }); } +// +// --- DRAG & DROP SUPPORT FOR FILE ROWS --- +// +function fileDragStartHandler(event) { + const row = event.currentTarget; + // Check if multiple file checkboxes are selected. + const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); + let fileNames = []; + if (selectedCheckboxes.length > 1) { + // Gather file names from all selected rows. + selectedCheckboxes.forEach(chk => { + const parentRow = chk.closest("tr"); + if (parentRow) { + const cell = parentRow.querySelector("td:nth-child(2)"); + if (cell) fileNames.push(cell.textContent.trim()); + } + }); + } else { + // Only one file is selected (or none), so get file name from the current row. + const fileNameCell = row.querySelector("td:nth-child(2)"); + if (fileNameCell) { + fileNames.push(fileNameCell.textContent.trim()); + } + } + if (fileNames.length === 0) return; + const dragData = { + files: fileNames, // use an array of file names + sourceFolder: window.currentFolder || "root" + }; + event.dataTransfer.setData("application/json", JSON.stringify(dragData)); + + // (Keep your custom drag image code here.) + let dragImage; + if (fileNames.length > 1) { + 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 countSpan = document.createElement("span"); + countSpan.textContent = fileNames.length + " files"; + 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); + event.dataTransfer.setDragImage(dragImage, 5, 5); + setTimeout(() => { + document.body.removeChild(dragImage); + }, 0); +} + +// +// --- RENDER FILE TABLE (TABLE VIEW) --- +// export function renderFileTable(folder) { const fileListContainer = document.getElementById("fileList"); const searchTerm = window.currentSearchTerm || ""; - const itemsPerPageSetting = parseInt(localStorage.getItem('itemsPerPage') || '10', 10); + const itemsPerPageSetting = parseInt(localStorage.getItem("itemsPerPage") || "10", 10); let currentPage = window.currentPage || 1; const filteredFiles = fileData.filter(file => @@ -347,7 +420,7 @@ export function renderFileTable(folder) { window.currentPage = currentPage; } - const folderPath = (folder === "root") + const folderPath = folder === "root" ? "uploads/" : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; @@ -356,18 +429,14 @@ export function renderFileTable(folder) { totalPages, searchTerm }); - let headerHTML = buildFileTableHeader(sortOrder); - // Do not add a separate share column; share button goes into the actions cell. - const startIndex = (currentPage - 1) * itemsPerPageSetting; const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); - let rowsHTML = ""; if (totalFiles > 0) { filteredFiles.slice(startIndex, endIndex).forEach(file => { let rowHTML = buildFileTableRow(file, folderPath); - // Insert share button into the actions container. + // Insert share button into the actions cell. rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `$1`); @@ -377,9 +446,7 @@ export function renderFileTable(folder) { rowsHTML += `No files found.`; } rowsHTML += ""; - const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); - fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; createViewToggleButton(); @@ -404,14 +471,13 @@ export function renderFileTable(folder) { }); }); - document.querySelectorAll('#fileList .file-checkbox').forEach(checkbox => { - checkbox.addEventListener('change', function (e) { + document.querySelectorAll("#fileList .file-checkbox").forEach(checkbox => { + checkbox.addEventListener("change", function (e) { updateRowHighlight(e.target); updateFileActionButtons(); }); }); - // Bind share button events in table view. document.querySelectorAll(".share-btn").forEach(btn => { btn.addEventListener("click", function (e) { e.stopPropagation(); @@ -424,17 +490,25 @@ export function renderFileTable(folder) { }); updateFileActionButtons(); + + // Add drag-and-drop support for each table row. + document.querySelectorAll("#fileList tbody tr").forEach(row => { + row.setAttribute("draggable", "true"); + row.addEventListener("dragstart", fileDragStartHandler); + }); } +// +// --- RENDER GALLERY VIEW --- +// export function renderGalleryView(folder) { const fileListContainer = document.getElementById("fileList"); - const folderPath = (folder === "root") + const folderPath = folder === "root" ? "uploads/" : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; - // Use CSS Grid for gallery layout. const gridStyle = "display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px;"; let galleryHTML = `"; fileListContainer.innerHTML = galleryHTML; + // Re-bind share button events if necessary. document.querySelectorAll(".gallery-share-btn").forEach(btn => { btn.addEventListener("click", function (e) { e.stopPropagation(); @@ -482,11 +557,14 @@ export function renderGalleryView(folder) { } }); }); - + createViewToggleButton(); updateFileActionButtons(); } +// +// --- SORT FILES & PARSE DATE --- +// export function sortFiles(column, folder) { if (sortOrder.column === column) { sortOrder.ascending = !sortOrder.ascending; @@ -562,6 +640,9 @@ export function canEditFile(fileName) { return allowedExtensions.includes(ext); } +// +// --- FILE ACTIONS: DELETE, DOWNLOAD, COPY, MOVE --- +// export function handleDeleteSelected(e) { e.preventDefault(); e.stopImmediatePropagation(); @@ -701,32 +782,44 @@ export function handleCopySelected(e) { export async function loadCopyMoveFolderListForModal(dropdownId) { try { - const response = await fetch('getFolderList.php'); + const response = await fetch("getFolderList.php"); let folders = await response.json(); if (Array.isArray(folders) && folders.length && typeof folders[0] === "object" && folders[0].folder) { folders = folders.map(item => item.folder); } folders = folders.filter(folder => folder !== "root"); - const folderSelect = document.getElementById(dropdownId); - folderSelect.innerHTML = ''; - const rootOption = document.createElement('option'); - rootOption.value = 'root'; - rootOption.textContent = '(Root)'; + folderSelect.innerHTML = ""; + const rootOption = document.createElement("option"); + rootOption.value = "root"; + rootOption.textContent = "(Root)"; folderSelect.appendChild(rootOption); if (Array.isArray(folders) && folders.length > 0) { folders.forEach(folder => { - const option = document.createElement('option'); + const option = document.createElement("option"); option.value = folder; option.textContent = formatFolderName(folder); folderSelect.appendChild(option); }); } } catch (error) { - console.error('Error loading folder list for modal:', error); + console.error("Error loading folder list for modal:", error); } } +export function handleMoveSelected(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (checkboxes.length === 0) { + showToast("No files selected for moving."); + return; + } + window.filesToMove = Array.from(checkboxes).map(chk => chk.value); + document.getElementById("moveFilesModal").style.display = "block"; + loadCopyMoveFolderListForModal("moveTargetFolder"); +} + document.addEventListener("DOMContentLoaded", function () { const cancelCopy = document.getElementById("cancelCopyFiles"); if (cancelCopy) { @@ -754,10 +847,10 @@ document.addEventListener("DOMContentLoaded", function () { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, - body: JSON.stringify({ - source: window.currentFolder, - files: window.filesToCopy, - destination: targetFolder + body: JSON.stringify({ + source: window.currentFolder, + files: window.filesToCopy, + destination: targetFolder }) }) .then(response => response.json()) @@ -778,19 +871,6 @@ document.addEventListener("DOMContentLoaded", function () { } }); -export function handleMoveSelected(e) { - e.preventDefault(); - e.stopImmediatePropagation(); - const checkboxes = document.querySelectorAll(".file-checkbox:checked"); - if (checkboxes.length === 0) { - showToast("No files selected for moving."); - return; - } - window.filesToMove = Array.from(checkboxes).map(chk => chk.value); - document.getElementById("moveFilesModal").style.display = "block"; - loadCopyMoveFolderListForModal("moveTargetFolder"); -} - document.addEventListener("DOMContentLoaded", function () { const cancelMove = document.getElementById("cancelMoveFiles"); if (cancelMove) { @@ -818,10 +898,10 @@ document.addEventListener("DOMContentLoaded", function () { "Content-Type": "application/json", "X-CSRF-Token": window.csrfToken }, - body: JSON.stringify({ - source: window.currentFolder, - files: window.filesToMove, - destination: targetFolder + body: JSON.stringify({ + source: window.currentFolder, + files: window.filesToMove, + destination: targetFolder }) }) .then(response => response.json()) @@ -842,20 +922,74 @@ document.addEventListener("DOMContentLoaded", function () { } }); -// Helper for CodeMirror editor mode based on file extension. +// +// --- FOLDER TREE DRAG & DROP SUPPORT --- +// When a draggable file is dragged over a folder node, allow the drop and highlight it. +function folderDragOverHandler(event) { + event.preventDefault(); + event.currentTarget.classList.add("drop-hover"); +} + +function folderDragLeaveHandler(event) { + event.currentTarget.classList.remove("drop-hover"); +} + +function folderDropHandler(event) { + event.preventDefault(); + event.currentTarget.classList.remove("drop-hover"); + const dropFolder = event.currentTarget.getAttribute("data-folder"); + let dragData; + try { + dragData = JSON.parse(event.dataTransfer.getData("application/json")); + } catch (e) { + console.error("Invalid drag data"); + return; + } + if (!dragData || !dragData.fileName) return; + fetch("moveFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") + }, + body: JSON.stringify({ + source: dragData.sourceFolder, + files: [dragData.fileName], + destination: dropFolder + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(`File "${dragData.fileName}" moved successfully to ${dropFolder}!`); + loadFileList(dragData.sourceFolder); + } else { + showToast("Error moving file: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error moving file via drop:", error); + showToast("Error moving file."); + }); +} + +// +// --- CODEMIRROR EDITOR & UTILITY FUNCTIONS --- +// function getModeForFile(fileName) { const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase(); switch (ext) { - case 'css': + case "css": return "css"; - case 'json': + case "json": return { name: "javascript", json: true }; - case 'js': + case "js": return "javascript"; - case 'html': - case 'htm': + case "html": + case "htm": return "text/html"; - case 'xml': + case "xml": return "xml"; default: return "text/plain"; @@ -866,7 +1000,7 @@ function adjustEditorSize() { const modal = document.querySelector(".editor-modal"); if (modal && window.currentEditor) { const modalHeight = modal.getBoundingClientRect().height || 600; - const newEditorHeight = Math.max(modalHeight * 0.80, 5) + "px"; + const newEditorHeight = Math.max(modalHeight * 0.8, 5) + "px"; window.currentEditor.setSize("100%", newEditorHeight); } } @@ -885,7 +1019,7 @@ export function editFile(fileName, folder) { existingEditor.remove(); } const folderUsed = folder || window.currentFolder || "root"; - const folderPath = (folderUsed === "root") + const folderPath = folderUsed === "root" ? "uploads/" : "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/"; const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime(); @@ -1107,15 +1241,24 @@ document.addEventListener("DOMContentLoaded", () => { }); window.renameFile = renameFile; - window.changePage = function (newPage) { window.currentPage = newPage; renderFileTable(window.currentFolder); }; - window.changeItemsPerPage = function (newCount) { window.itemsPerPage = parseInt(newCount); window.currentPage = 1; renderFileTable(window.currentFolder); }; -window.previewFile = previewFile; \ No newline at end of file +window.previewFile = previewFile; + +// +// --- Expose Drag-Drop Support for Folder Tree Nodes --- +// (Attach dragover, dragleave, and drop events to folder tree nodes) +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".folder-option").forEach(el => { + el.addEventListener("dragover", folderDragOverHandler); + el.addEventListener("dragleave", folderDragLeaveHandler); + el.addEventListener("drop", folderDropHandler); + }); +}); \ No newline at end of file diff --git a/folderManager.js b/folderManager.js index 89b6f3f..9deeba7 100644 --- a/folderManager.js +++ b/folderManager.js @@ -114,6 +114,64 @@ function expandTreePath(path) { }); } +// ---------------------- +// Drag & Drop Support for Folder Tree Nodes +// ---------------------- + +// When a draggable file is dragged over a folder node, allow the drop and add a visual cue. +function folderDragOverHandler(event) { + event.preventDefault(); + event.currentTarget.classList.add("drop-hover"); +} + +// Remove the visual cue when the drag leaves. +function folderDragLeaveHandler(event) { + event.currentTarget.classList.remove("drop-hover"); +} + +// When a file is dropped onto a folder node, send a move request. +function folderDropHandler(event) { + event.preventDefault(); + event.currentTarget.classList.remove("drop-hover"); + const dropFolder = event.currentTarget.getAttribute("data-folder"); + let dragData; + try { + dragData = JSON.parse(event.dataTransfer.getData("application/json")); + } catch (e) { + console.error("Invalid drag data"); + return; + } + // Use the files array if present, or fall back to a single file. + const filesToMove = dragData.files ? dragData.files : (dragData.fileName ? [dragData.fileName] : []); + if (filesToMove.length === 0) return; + fetch("moveFiles.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content") + }, + body: JSON.stringify({ + source: dragData.sourceFolder, + files: filesToMove, + destination: dropFolder + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast(`File(s) moved successfully to ${dropFolder}!`); + loadFileList(dragData.sourceFolder); + } else { + showToast("Error moving files: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error moving files via drop:", error); + showToast("Error moving files."); + }); +} + // ---------------------- // Main Folder Tree Rendering and Event Binding // ---------------------- @@ -123,7 +181,6 @@ export async function loadFolderTree(selectedFolder) { if (response.status === 401) { console.error("Unauthorized: Please log in to view folders."); showToast("Session expired. Please log in again."); - // Redirect to logout.php to clear the session; this can trigger a login process. window.location.href = "logout.php"; return; } @@ -163,6 +220,13 @@ export async function loadFolderTree(selectedFolder) { } container.innerHTML = html; + // Attach drag-and-drop event listeners to folder nodes. + container.querySelectorAll(".folder-option").forEach(el => { + el.addEventListener("dragover", folderDragOverHandler); + el.addEventListener("dragleave", folderDragLeaveHandler); + el.addEventListener("drop", folderDropHandler); + }); + // Determine current folder. if (selectedFolder) { window.currentFolder = selectedFolder; @@ -282,8 +346,7 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function }); document.getElementById("submitRenameFolder").addEventListener("click", function (event) { - event.preventDefault(); // Prevent default form submission - + event.preventDefault(); const selectedFolder = window.currentFolder || "root"; const newNameBasename = document.getElementById("newRenameFolderName").value.trim(); if (!newNameBasename || newNameBasename === selectedFolder.split("/").pop()) { @@ -292,18 +355,14 @@ document.getElementById("submitRenameFolder").addEventListener("click", function } const parentPath = getParentFolder(selectedFolder); const newFolderFull = parentPath === "root" ? newNameBasename : parentPath + "/" + newNameBasename; - - // Read the CSRF token from the meta tag const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); if (!csrfToken) { showToast("CSRF token not loaded yet! Please try again."); return; } - - // Send the rename request with the CSRF token in a custom header fetch("renameFolder.php", { method: "POST", - credentials: "include", // ensure cookies (and session) are sent + credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken @@ -345,9 +404,7 @@ document.getElementById("cancelDeleteFolder").addEventListener("click", function document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; - // Read CSRF token from the meta tag const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("deleteFolder.php", { method: "POST", headers: { @@ -393,9 +450,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", function if (selectedFolder && selectedFolder !== "root") { fullFolderName = selectedFolder + "/" + folderInput; } - // Read CSRF token from the meta tag const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); - fetch("createFolder.php", { method: "POST", headers: { diff --git a/main.js b/main.js index 800b05a..989b556 100644 --- a/main.js +++ b/main.js @@ -17,7 +17,6 @@ import { loadFolderTree } from './folderManager.js'; import { initUpload } from './upload.js'; import { initAuth, checkAuthentication } from './auth.js'; - function loadCsrfToken() { fetch('token.php', { credentials: 'include' }) .then(response => response.json()) @@ -65,12 +64,9 @@ document.addEventListener("DOMContentLoaded", function () { initAuth(); // --- Dark Mode Persistence --- - // Get the dark mode toggle button. const darkModeToggle = document.getElementById("darkModeToggle"); - // Retrieve stored user preference (if any). const storedDarkMode = localStorage.getItem("darkMode"); - // Apply stored preference; if none, fall back to OS setting. if (storedDarkMode === "true") { document.body.classList.add("dark-mode"); } else if (storedDarkMode === "false") { @@ -83,13 +79,11 @@ document.addEventListener("DOMContentLoaded", function () { } } - // Set the initial button label. if (darkModeToggle) { darkModeToggle.textContent = document.body.classList.contains("dark-mode") ? "Light Mode" : "Dark Mode"; - // When clicked, toggle dark mode and store preference. darkModeToggle.addEventListener("click", function () { if (document.body.classList.contains("dark-mode")) { document.body.classList.remove("dark-mode"); @@ -103,7 +97,6 @@ document.addEventListener("DOMContentLoaded", function () { }); } - // Listen for OS theme changes if no user preference is set. if (localStorage.getItem("darkMode") === null && window.matchMedia) { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (event) => { if (event.matches) { @@ -134,4 +127,17 @@ document.addEventListener("DOMContentLoaded", function () { console.warn("User not authenticated. Data loading deferred."); } }); + + // --- Auto-scroll During Drag --- + // Adjust these values as needed: + const SCROLL_THRESHOLD = 50; // pixels from edge to start scrolling + const SCROLL_SPEED = 10; // pixels to scroll per event + + document.addEventListener("dragover", function(e) { + if (e.clientY < SCROLL_THRESHOLD) { + window.scrollBy(0, -SCROLL_SPEED); + } else if (e.clientY > window.innerHeight - SCROLL_THRESHOLD) { + window.scrollBy(0, SCROLL_SPEED); + } + }); }); \ No newline at end of file diff --git a/moveFiles.php b/moveFiles.php index ff66023..6c4a0de 100644 --- a/moveFiles.php +++ b/moveFiles.php @@ -102,7 +102,7 @@ $srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMet $destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; $errors = []; -$safeFileNamePattern = '/^[A-Za-z0-9_\-\. ]+$/'; +$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/'; foreach ($data['files'] as $fileName) { // Save the original name for metadata lookup. diff --git a/styles.css b/styles.css index b23cdb5..b334ef0 100644 --- a/styles.css +++ b/styles.css @@ -1630,4 +1630,9 @@ body.dark-mode .CodeMirror-matchingbracket { .gallery-nav-btn.right { right: 10px; left: auto; +} + +.drop-hover { + background-color: #e0e0e0; + border: 1px dashed #666; } \ No newline at end of file