Files
FileRise/fileManager.js
2025-03-19 17:06:39 -04:00

1121 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
escapeHTML,
debounce,
buildSearchAndPaginationControls,
buildFileTableHeader,
buildFileTableRow,
buildBottomControls,
updateFileActionButtons,
showToast,
updateRowHighlight,
toggleRowSelection,
previewFile as originalPreviewFile
} from './domUtils.js';
export let fileData = [];
export let sortOrder = { column: "uploaded", ascending: true };
window.itemsPerPage = window.itemsPerPage || 10;
window.currentPage = window.currentPage || 1;
window.viewMode = localStorage.getItem("viewMode") || "table"; // "table" or "gallery"
// ==============================
// VIEW MODE TOGGLE BUTTON
// ==============================
function createViewToggleButton() {
let toggleBtn = document.getElementById("toggleViewBtn");
if (!toggleBtn) {
toggleBtn = document.createElement("button");
toggleBtn.id = "toggleViewBtn";
toggleBtn.classList.add("btn", "btn-secondary");
const titleElem = document.getElementById("fileListTitle");
if (titleElem) {
titleElem.parentNode.insertBefore(toggleBtn, titleElem.nextSibling);
}
}
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View";
toggleBtn.onclick = () => {
window.viewMode = window.viewMode === "gallery" ? "table" : "gallery";
localStorage.setItem("viewMode", window.viewMode);
loadFileList(window.currentFolder);
toggleBtn.textContent = window.viewMode === "gallery" ? "Switch to Table View" : "Switch to Gallery View";
};
return toggleBtn;
}
window.createViewToggleButton = createViewToggleButton;
// -----------------------------
// Helper: formatFolderName
// -----------------------------
function formatFolderName(folder) {
if (folder === "root") return "(Root)";
return folder
.replace(/[_-]+/g, " ")
.replace(/\b\w/g, char => char.toUpperCase());
}
// Expose inline DOM helpers.
window.toggleRowSelection = toggleRowSelection;
window.updateRowHighlight = updateRowHighlight;
// ==============================================
// FEATURE: Public File Sharing Modal
// ==============================================
function openShareModal(file, folder) {
const existing = document.getElementById("shareModal");
if (existing) existing.remove();
const modal = document.createElement("div");
modal.id = "shareModal";
modal.classList.add("modal");
modal.innerHTML = `
<div class="modal-content share-modal-content" style="width: 600px; max-width:90vw;">
<div class="modal-header">
<h3>Share File: ${escapeHTML(file.name)}</h3>
<span class="close-image-modal" id="closeShareModal" title="Close">&times;</span>
</div>
<div class="modal-body">
<p>Set Expiration:</p>
<select id="shareExpiration">
<option value="30">30 minutes</option>
<option value="60" selected>60 minutes</option>
<option value="120">120 minutes</option>
<option value="180">180 minutes</option>
<option value="240">240 minutes</option>
<option value="1440">1 Day</option>
</select>
<p>Password (optional):</p>
<input type="text" id="sharePassword" placeholder="No password by default" style="width: 100%;"/>
<br>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">Generate Share Link</button>
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
<p>Shareable Link:</p>
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">Copy Link</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = "block";
document.getElementById("closeShareModal").addEventListener("click", () => {
modal.remove();
});
document.getElementById("generateShareLinkBtn").addEventListener("click", () => {
const expiration = document.getElementById("shareExpiration").value;
const password = document.getElementById("sharePassword").value;
fetch("createShareLink.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({
folder: folder,
file: file.name,
expirationMinutes: parseInt(expiration),
password: password
})
})
.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");
const shareUrl = `${shareEndpoint}?token=${encodeURIComponent(data.token)}`;
const displayDiv = document.getElementById("shareLinkDisplay");
const inputField = document.getElementById("shareLinkInput");
inputField.value = shareUrl;
displayDiv.style.display = "block";
} else {
showToast("Error generating share link: " + (data.error || "Unknown error"));
}
})
.catch(err => {
console.error("Error generating share link:", err);
showToast("Error generating share link.");
});
});
document.getElementById("copyShareLinkBtn").addEventListener("click", () => {
const input = document.getElementById("shareLinkInput");
input.select();
document.execCommand("copy");
showToast("Link copied to clipboard!");
});
}
// ==============================================
// 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) {
modal = document.createElement("div");
modal.id = "filePreviewModal";
Object.assign(modal.style, {
position: "fixed",
top: "0",
left: "0",
width: "100vw",
height: "100vh",
backgroundColor: "rgba(0,0,0,0.7)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: "1000"
});
modal.innerHTML = `
<div class="modal-content image-preview-modal-content" style="position: relative; max-width: 90vw; max-height: 90vh;">
<span id="closeFileModal" class="close-image-modal" style="position: absolute; top: 10px; right: 10px; font-size: 24px; cursor: pointer;">&times;</span>
<h4 class="image-modal-header" style="text-align: center; margin-top: 40px;"></h4>
<div class="file-preview-container" style="position: relative; text-align: center;"></div>
</div>`;
document.body.appendChild(modal);
document.getElementById("closeFileModal").addEventListener("click", function () {
modal.style.display = "none";
});
modal.addEventListener("click", function (e) {
if (e.target === modal) {
modal.style.display = "none";
}
});
}
modal.querySelector("h4").textContent = fileName;
const container = modal.querySelector(".file-preview-container");
container.innerHTML = "";
const extension = fileName.split('.').pop().toLowerCase();
const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(fileName);
if (isImage) {
const img = document.createElement("img");
img.src = fileUrl;
img.className = "image-modal-img";
img.style.maxWidth = "80vw";
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;
modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName);
const prevBtn = document.createElement("button");
prevBtn.textContent = "";
prevBtn.className = "gallery-nav-btn";
prevBtn.style.cssText = "position: absolute; top: 50%; left: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
prevBtn.addEventListener("click", function (e) {
e.stopPropagation();
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();
});
const nextBtn = document.createElement("button");
nextBtn.textContent = "";
nextBtn.className = "gallery-nav-btn";
nextBtn.style.cssText = "position: absolute; top: 50%; right: 10px; transform: translateY(-50%); background: transparent; border: none; color: white; font-size: 48px; cursor: pointer;";
nextBtn.addEventListener("click", function (e) {
e.stopPropagation();
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();
});
container.appendChild(prevBtn);
container.appendChild(nextBtn);
}
} else {
if (extension === "pdf") {
const embed = document.createElement("embed");
const separator = fileUrl.indexOf('?') === -1 ? '?' : '&';
embed.src = fileUrl + separator + 't=' + new Date().getTime();
embed.type = "application/pdf";
embed.style.width = "80vw";
embed.style.height = "80vh";
embed.style.border = "none";
container.appendChild(embed);
} else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
video.className = "image-modal-img";
container.appendChild(video);
} else {
container.textContent = "Preview not available for this file type.";
}
}
modal.style.display = "flex";
}
export function previewFile(fileUrl, fileName) {
enhancedPreviewFile(fileUrl, fileName);
}
// ==============================================
// ORIGINAL FILE MANAGER FUNCTIONS
// ==============================================
export function loadFileList(folderParam) {
const folder = folderParam || "root";
const fileListContainer = document.getElementById("fileList");
fileListContainer.style.visibility = "hidden";
fileListContainer.innerHTML = "<div class='loader'>Loading files...</div>";
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();
})
.then(data => {
fileListContainer.innerHTML = "";
if (data.files && data.files.length > 0) {
data.files = data.files.map(file => {
file.fullName = (file.path || file.name).trim().toLowerCase();
file.editable = canEditFile(file.name);
file.folder = folder;
if (!file.type && /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
file.type = "image";
}
return file;
});
fileData = data.files;
if (window.viewMode === "gallery") {
renderGalleryView(folder);
} else {
renderFileTable(folder);
}
} else {
fileListContainer.textContent = "No files found.";
updateFileActionButtons();
}
return data.files || [];
})
.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.";
}
return [];
})
.finally(() => {
fileListContainer.style.visibility = "visible";
});
}
export function renderFileTable(folder) {
const fileListContainer = document.getElementById("fileList");
const searchTerm = window.currentSearchTerm || "";
const itemsPerPageSetting = parseInt(localStorage.getItem('itemsPerPage') || '10', 10);
let currentPage = window.currentPage || 1;
const filteredFiles = fileData.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const totalFiles = filteredFiles.length;
const totalPages = Math.ceil(totalFiles / itemsPerPageSetting);
if (currentPage > totalPages) {
currentPage = totalPages > 0 ? totalPages : 1;
window.currentPage = currentPage;
}
const folderPath = (folder === "root")
? "uploads/"
: "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/";
const topControlsHTML = buildSearchAndPaginationControls({
currentPage,
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 = "<tbody>";
if (totalFiles > 0) {
filteredFiles.slice(startIndex, endIndex).forEach(file => {
let rowHTML = buildFileTableRow(file, folderPath);
// Insert share button into the actions container.
rowHTML = rowHTML.replace(/(<\/div>\s*<\/td>\s*<\/tr>)/, `<button class="share-btn btn btn-sm btn-secondary" data-file="${escapeHTML(file.name)}" title="Share">
<i class="material-icons">share</i>
</button>$1`);
rowsHTML += rowHTML;
});
} else {
rowsHTML += `<tr><td colspan="8">No files found.</td></tr>`;
}
rowsHTML += "</tbody></table>";
const bottomControlsHTML = buildBottomControls(itemsPerPageSetting);
fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML;
createViewToggleButton();
const newSearchInput = document.getElementById("searchInput");
if (newSearchInput) {
newSearchInput.addEventListener("input", debounce(function () {
window.currentSearchTerm = newSearchInput.value;
window.currentPage = 1;
renderFileTable(folder);
setTimeout(() => {
newSearchInput.focus();
newSearchInput.setSelectionRange(newSearchInput.value.length, newSearchInput.value.length);
}, 0);
}, 300));
}
document.querySelectorAll("table.table thead th[data-column]").forEach(cell => {
cell.addEventListener("click", function () {
const column = this.getAttribute("data-column");
sortFiles(column, folder);
});
});
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();
const fileName = this.getAttribute("data-file");
const file = fileData.find(f => f.name === fileName);
if (file) {
openShareModal(file, folder);
}
});
});
updateFileActionButtons();
}
export function renderGalleryView(folder) {
const fileListContainer = document.getElementById("fileList");
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 = `<div class="gallery-container" style="${gridStyle}">`;
fileData.forEach((file, index) => {
let thumbnail;
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
thumbnail = `<img src="${folderPath + encodeURIComponent(file.name)}?t=${new Date().getTime()}" class="gallery-thumbnail" alt="${escapeHTML(file.name)}" style="max-width: 100%; max-height: 150px; display: block; margin: 0 auto;">`;
} else {
thumbnail = `<span class="material-icons gallery-icon">insert_drive_file</span>`;
}
galleryHTML += `<div class="gallery-card" style="border: 1px solid #ccc; padding: 5px; text-align: center;">
<div class="gallery-preview" style="cursor: pointer;" onclick="previewFile('${folderPath + encodeURIComponent(file.name)}?t=' + new Date().getTime(), '${file.name}')">
${thumbnail}
</div>
<div class="gallery-info" style="margin-top: 5px;">
<span class="gallery-file-name" style="display: block;">${escapeHTML(file.name)}</span>
<div class="button-wrap" style="display: flex; justify-content: center; gap: 5px;">
<a class="btn btn-sm btn-success download-btn"
href="download.php?folder=${encodeURIComponent(file.folder || 'root')}&file=${encodeURIComponent(file.name)}"
title="Download">
<i class="material-icons">file_download</i>
</a>
${file.editable ? `
<button class="btn btn-sm btn-primary" onclick='editFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Edit">
<i class="material-icons">edit</i>
</button>
` : ""}
<button class="btn btn-sm btn-warning rename-btn" onclick='renameFile(${JSON.stringify(file.name)}, ${JSON.stringify(file.folder || "root")})' title="Rename">
<i class="material-icons">drive_file_rename_outline</i>
</button>
<button class="btn btn-sm btn-secondary share-btn" onclick='openShareModal(${JSON.stringify(file)}, ${JSON.stringify(folder)})' title="Share">
<i class="material-icons">share</i>
</button>
</div>
</div>
</div>`;
});
galleryHTML += "</div>";
fileListContainer.innerHTML = galleryHTML;
document.querySelectorAll(".gallery-share-btn").forEach(btn => {
btn.addEventListener("click", function (e) {
e.stopPropagation();
const fileName = this.getAttribute("data-file");
const folder = this.getAttribute("data-folder");
const file = fileData.find(f => f.name === fileName);
if (file) {
openShareModal(file, folder);
}
});
});
createViewToggleButton();
updateFileActionButtons();
}
export function sortFiles(column, folder) {
if (sortOrder.column === column) {
sortOrder.ascending = !sortOrder.ascending;
} else {
sortOrder.column = column;
sortOrder.ascending = true;
}
fileData.sort((a, b) => {
let valA = a[column] || "";
let valB = b[column] || "";
if (column === "modified" || column === "uploaded") {
const parsedA = parseCustomDate(valA);
const parsedB = parseCustomDate(valB);
valA = parsedA;
valB = parsedB;
} else if (typeof valA === "string") {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
}
if (valA < valB) return sortOrder.ascending ? -1 : 1;
if (valA > valB) return sortOrder.ascending ? 1 : -1;
return 0;
});
if (window.viewMode === "gallery") {
renderGalleryView(folder);
} else {
renderFileTable(folder);
}
}
function parseCustomDate(dateStr) {
dateStr = dateStr.replace(/\s+/g, " ").trim();
const parts = dateStr.split(" ");
if (parts.length !== 2) {
return new Date(dateStr).getTime();
}
const datePart = parts[0];
const timePart = parts[1];
const dateComponents = datePart.split("/");
if (dateComponents.length !== 3) {
return new Date(dateStr).getTime();
}
let month = parseInt(dateComponents[0], 10);
let day = parseInt(dateComponents[1], 10);
let year = parseInt(dateComponents[2], 10);
if (year < 100) {
year += 2000;
}
const timeRegex = /^(\d{1,2}):(\d{2})(AM|PM)$/i;
const match = timePart.match(timeRegex);
if (!match) {
return new Date(dateStr).getTime();
}
let hour = parseInt(match[1], 10);
const minute = parseInt(match[2], 10);
const period = match[3].toUpperCase();
if (period === "PM" && hour !== 12) {
hour += 12;
}
if (period === "AM" && hour === 12) {
hour = 0;
}
return new Date(year, month - 1, day, hour, minute).getTime();
}
export function canEditFile(fileName) {
const allowedExtensions = [
"txt", "html", "htm", "css", "js", "json", "xml",
"md", "py", "ini", "csv", "log", "conf", "config", "bat",
"rtf", "doc", "docx"
];
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
return allowedExtensions.includes(ext);
}
export function handleDeleteSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) {
showToast("No files selected.");
return;
}
window.filesToDelete = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("deleteFilesMessage").textContent =
"Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?";
document.getElementById("deleteFilesModal").style.display = "block";
}
document.addEventListener("DOMContentLoaded", function () {
const cancelDelete = document.getElementById("cancelDeleteFiles");
if (cancelDelete) {
cancelDelete.addEventListener("click", function () {
document.getElementById("deleteFilesModal").style.display = "none";
window.filesToDelete = [];
});
}
const confirmDelete = document.getElementById("confirmDeleteFiles");
if (confirmDelete) {
confirmDelete.addEventListener("click", function () {
fetch("deleteFiles.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({ folder: window.currentFolder, files: window.filesToDelete })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files deleted successfully!");
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not delete files"));
}
})
.catch(error => console.error("Error deleting files:", error))
.finally(() => {
document.getElementById("deleteFilesModal").style.display = "none";
window.filesToDelete = [];
});
});
}
});
export function handleDownloadZipSelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) {
showToast("No files selected for download.");
return;
}
window.filesToDownload = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("downloadZipModal").style.display = "block";
}
document.addEventListener("DOMContentLoaded", function () {
const cancelDownloadZip = document.getElementById("cancelDownloadZip");
if (cancelDownloadZip) {
cancelDownloadZip.addEventListener("click", function () {
document.getElementById("downloadZipModal").style.display = "none";
});
}
const confirmDownloadZip = document.getElementById("confirmDownloadZip");
if (confirmDownloadZip) {
confirmDownloadZip.addEventListener("click", function () {
let zipName = document.getElementById("zipFileNameInput").value.trim();
if (!zipName) {
showToast("Please enter a name for the zip file.");
return;
}
if (!zipName.toLowerCase().endsWith(".zip")) {
zipName += ".zip";
}
document.getElementById("downloadZipModal").style.display = "none";
const folder = window.currentFolder || "root";
fetch("downloadZip.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({ folder: folder, files: window.filesToDownload })
})
.then(response => {
if (!response.ok) {
return response.text().then(text => {
throw new Error("Failed to create zip file: " + text);
});
}
return response.blob();
})
.then(blob => {
if (!blob || blob.size === 0) {
throw new Error("Received empty zip file.");
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = zipName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
showToast("Download started.");
})
.catch(error => {
console.error("Error downloading zip:", error);
showToast("Error downloading selected files as zip: " + error.message);
});
});
}
});
export function handleCopySelected(e) {
e.preventDefault();
e.stopImmediatePropagation();
const checkboxes = document.querySelectorAll(".file-checkbox:checked");
if (checkboxes.length === 0) {
showToast("No files selected for copying.", 5000);
return;
}
window.filesToCopy = Array.from(checkboxes).map(chk => chk.value);
document.getElementById("copyFilesModal").style.display = "block";
loadCopyMoveFolderListForModal("copyTargetFolder");
}
export async function loadCopyMoveFolderListForModal(dropdownId) {
try {
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.appendChild(rootOption);
if (Array.isArray(folders) && folders.length > 0) {
folders.forEach(folder => {
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);
}
}
document.addEventListener("DOMContentLoaded", function () {
const cancelCopy = document.getElementById("cancelCopyFiles");
if (cancelCopy) {
cancelCopy.addEventListener("click", function () {
document.getElementById("copyFilesModal").style.display = "none";
window.filesToCopy = [];
});
}
const confirmCopy = document.getElementById("confirmCopyFiles");
if (confirmCopy) {
confirmCopy.addEventListener("click", function () {
const targetFolder = document.getElementById("copyTargetFolder").value;
if (!targetFolder) {
showToast("Please select a target folder for copying.", 5000);
return;
}
if (targetFolder === window.currentFolder) {
showToast("Error: Cannot copy files to the same folder.");
return;
}
fetch("copyFiles.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({
source: window.currentFolder,
files: window.filesToCopy,
destination: targetFolder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files copied successfully!", 5000);
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not copy files"), 5000);
}
})
.catch(error => console.error("Error copying files:", error))
.finally(() => {
document.getElementById("copyFilesModal").style.display = "none";
window.filesToCopy = [];
});
});
}
});
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) {
cancelMove.addEventListener("click", function () {
document.getElementById("moveFilesModal").style.display = "none";
window.filesToMove = [];
});
}
const confirmMove = document.getElementById("confirmMoveFiles");
if (confirmMove) {
confirmMove.addEventListener("click", function () {
const targetFolder = document.getElementById("moveTargetFolder").value;
if (!targetFolder) {
showToast("Please select a target folder for moving.");
return;
}
if (targetFolder === window.currentFolder) {
showToast("Error: Cannot move files to the same folder.");
return;
}
fetch("moveFiles.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({
source: window.currentFolder,
files: window.filesToMove,
destination: targetFolder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("Selected files moved successfully!");
loadFileList(window.currentFolder);
} else {
showToast("Error: " + (data.error || "Could not move files"));
}
})
.catch(error => console.error("Error moving files:", error))
.finally(() => {
document.getElementById("moveFilesModal").style.display = "none";
window.filesToMove = [];
});
});
}
});
// Helper for CodeMirror editor mode based on file extension.
function getModeForFile(fileName) {
const ext = fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase();
switch (ext) {
case 'css':
return "css";
case 'json':
return { name: "javascript", json: true };
case 'js':
return "javascript";
case 'html':
case 'htm':
return "text/html";
case 'xml':
return "xml";
default:
return "text/plain";
}
}
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";
window.currentEditor.setSize("100%", newEditorHeight);
}
}
function observeModalResize(modal) {
if (!modal) return;
const resizeObserver = new ResizeObserver(() => {
adjustEditorSize();
});
resizeObserver.observe(modal);
}
export function editFile(fileName, folder) {
let existingEditor = document.getElementById("editorContainer");
if (existingEditor) {
existingEditor.remove();
}
const folderUsed = folder || window.currentFolder || "root";
const folderPath = (folderUsed === "root")
? "uploads/"
: "uploads/" + folderUsed.split("/").map(encodeURIComponent).join("/") + "/";
const fileUrl = folderPath + encodeURIComponent(fileName) + "?t=" + new Date().getTime();
fetch(fileUrl, { method: "HEAD" })
.then(response => {
const contentLength = response.headers.get("Content-Length");
if (!contentLength || parseInt(contentLength) > 10485760) {
showToast("This file is larger than 10 MB and cannot be edited in the browser.");
throw new Error("File too large.");
}
return fetch(fileUrl);
})
.then(response => {
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
return response.text();
})
.then(content => {
const modal = document.createElement("div");
modal.id = "editorContainer";
modal.classList.add("modal", "editor-modal");
modal.innerHTML = `
<div class="editor-header">
<h3 class="editor-title">Editing: ${fileName}</h3>
<button id="closeEditorX" class="editor-close-btn">&times;</button>
</div>
<div id="editorControls" class="editor-controls">
<button id="decreaseFont" class="btn btn-sm btn-secondary">A-</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">A+</button>
</div>
<textarea id="fileEditor" class="editor-textarea">${content}</textarea>
<div class="editor-footer">
<button id="saveBtn" class="btn btn-primary">Save</button>
<button id="closeBtn" class="btn btn-secondary">Close</button>
</div>
`;
document.body.appendChild(modal);
modal.style.display = "block";
const mode = getModeForFile(fileName);
const isDarkMode = document.body.classList.contains("dark-mode");
const theme = isDarkMode ? "material-darker" : "default";
const editor = CodeMirror.fromTextArea(document.getElementById("fileEditor"), {
lineNumbers: true,
mode: mode,
theme: theme,
viewportMargin: Infinity
});
window.currentEditor = editor;
setTimeout(() => {
adjustEditorSize();
}, 50);
observeModalResize(modal);
let currentFontSize = 14;
editor.getWrapperElement().style.fontSize = currentFontSize + "px";
editor.refresh();
document.getElementById("closeEditorX").addEventListener("click", function () {
modal.remove();
});
document.getElementById("decreaseFont").addEventListener("click", function () {
currentFontSize = Math.max(8, currentFontSize - 2);
editor.getWrapperElement().style.fontSize = currentFontSize + "px";
editor.refresh();
});
document.getElementById("increaseFont").addEventListener("click", function () {
currentFontSize = Math.min(32, currentFontSize + 2);
editor.getWrapperElement().style.fontSize = currentFontSize + "px";
editor.refresh();
});
document.getElementById("saveBtn").addEventListener("click", function () {
saveFile(fileName, folderUsed);
});
document.getElementById("closeBtn").addEventListener("click", function () {
modal.remove();
});
function updateEditorTheme() {
const isDarkMode = document.body.classList.contains("dark-mode");
editor.setOption("theme", isDarkMode ? "material-darker" : "default");
}
document.getElementById("darkModeToggle").addEventListener("click", updateEditorTheme);
})
.catch(error => console.error("Error loading file:", error));
}
export function saveFile(fileName, folder) {
const editor = window.currentEditor;
if (!editor) {
console.error("Editor not found!");
return;
}
const folderUsed = folder || window.currentFolder || "root";
const fileDataObj = {
fileName: fileName,
content: editor.getValue(),
folder: folderUsed
};
fetch("saveFile.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify(fileDataObj)
})
.then(response => response.json())
.then(result => {
showToast(result.success || result.error);
document.getElementById("editorContainer")?.remove();
loadFileList(folderUsed);
})
.catch(error => console.error("Error saving file:", error));
}
export function displayFilePreview(file, container) {
container.style.display = "inline-block";
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name)) {
const img = document.createElement("img");
img.src = URL.createObjectURL(file);
img.classList.add("file-preview-img");
container.appendChild(img);
} else {
const iconSpan = document.createElement("span");
iconSpan.classList.add("material-icons", "file-icon");
iconSpan.textContent = "insert_drive_file";
container.appendChild(iconSpan);
}
}
export function initFileActions() {
const deleteSelectedBtn = document.getElementById("deleteSelectedBtn");
if (deleteSelectedBtn) {
deleteSelectedBtn.replaceWith(deleteSelectedBtn.cloneNode(true));
document.getElementById("deleteSelectedBtn").addEventListener("click", handleDeleteSelected);
}
const copySelectedBtn = document.getElementById("copySelectedBtn");
if (copySelectedBtn) {
copySelectedBtn.replaceWith(copySelectedBtn.cloneNode(true));
document.getElementById("copySelectedBtn").addEventListener("click", handleCopySelected);
}
const moveSelectedBtn = document.getElementById("moveSelectedBtn");
if (moveSelectedBtn) {
moveSelectedBtn.replaceWith(moveSelectedBtn.cloneNode(true));
document.getElementById("moveSelectedBtn").addEventListener("click", handleMoveSelected);
}
const downloadZipBtn = document.getElementById("downloadZipBtn");
if (downloadZipBtn) {
downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true));
document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected);
}
}
export function renameFile(oldName, folder) {
window.fileToRename = oldName;
window.fileFolder = folder || window.currentFolder || "root";
document.getElementById("newFileName").value = oldName;
document.getElementById("renameFileModal").style.display = "block";
}
document.addEventListener("DOMContentLoaded", () => {
const cancelBtn = document.getElementById("cancelRenameFile");
if (cancelBtn) {
cancelBtn.addEventListener("click", function () {
document.getElementById("renameFileModal").style.display = "none";
document.getElementById("newFileName").value = "";
});
}
const submitBtn = document.getElementById("submitRenameFile");
if (submitBtn) {
submitBtn.addEventListener("click", function () {
const newName = document.getElementById("newFileName").value.trim();
if (!newName || newName === window.fileToRename) {
document.getElementById("renameFileModal").style.display = "none";
return;
}
const folderUsed = window.fileFolder;
fetch("renameFile.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify({ folder: folderUsed, oldName: window.fileToRename, newName: newName })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast("File renamed successfully!");
loadFileList(folderUsed);
} else {
showToast("Error renaming file: " + (data.error || "Unknown error"));
}
})
.catch(error => {
console.error("Error renaming file:", error);
showToast("Error renaming file");
})
.finally(() => {
document.getElementById("renameFileModal").style.display = "none";
document.getElementById("newFileName").value = "";
});
});
}
});
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;