Refactor API endpoints and modularize controllers and models

This commit is contained in:
Ryan
2025-04-16 11:40:17 -04:00
committed by GitHub
parent 853d8835d9
commit ec5c3fc452
128 changed files with 8189 additions and 5318 deletions

437
public/js/filePreview.js Normal file
View File

@@ -0,0 +1,437 @@
// filePreview.js
import { escapeHTML, showToast } from './domUtils.js';
import { fileData } from './fileListView.js';
import { t } from './i18n.js';
export 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>${t("share_file")}: ${escapeHTML(file.name)}</h3>
<span class="close-image-modal" id="closeShareModal" title="Close">&times;</span>
</div>
<div class="modal-body">
<p>${t("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>${t("password_optional")}</p>
<input type="text" id="sharePassword" placeholder=${t("password_optional")} style="width: 100%;"/>
<br>
<button id="generateShareLinkBtn" class="btn btn-primary" style="margin-top:10px;">${t("generate_share_link")}</button>
<div id="shareLinkDisplay" style="margin-top: 10px; display:none;">
<p>${t("shareable_link")}</p>
<input type="text" id="shareLinkInput" readonly style="width:100%;"/>
<button id="copyShareLinkBtn" class="btn btn-primary" style="margin-top:5px;">${t("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("api/file/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) {
let shareEndpoint = document.querySelector('meta[name="share-url"]')
? document.querySelector('meta[name="share-url"]').getAttribute('content')
: (window.SHARE_URL || "api/file/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!");
});
}
export function previewFile(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"></h4>
<div class="file-preview-container" style="position: relative; text-align: center;"></div>
</div>`;
document.body.appendChild(modal);
function closeModal() {
const mediaElements = modal.querySelectorAll("video, audio");
mediaElements.forEach(media => {
media.pause();
if (media.tagName.toLowerCase() !== 'video') {
try { media.currentTime = 0; } catch (e) { }
}
});
modal.remove();
}
document.getElementById("closeFileModal").addEventListener("click", closeModal);
modal.addEventListener("click", function (e) {
if (e.target === modal) {
closeModal();
}
});
}
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) {
// Create the image element with default transform data.
const img = document.createElement("img");
img.src = fileUrl;
img.className = "image-modal-img";
img.style.maxWidth = "80vw";
img.style.maxHeight = "80vh";
img.style.transition = "transform 0.3s ease";
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.position = 'relative';
img.style.zIndex = '1';
// Filter gallery images for navigation.
const images = fileData.filter(file => /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(file.name));
// Create a flex wrapper to hold left panel, center image, and right panel.
const wrapper = document.createElement('div');
wrapper.className = 'image-wrapper';
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.justifyContent = 'center';
wrapper.style.position = 'relative';
// --- Left Panel: Contains Zoom controls (top) and Prev button (bottom) ---
const leftPanel = document.createElement('div');
leftPanel.className = 'left-panel';
leftPanel.style.display = 'flex';
leftPanel.style.flexDirection = 'column';
leftPanel.style.justifyContent = 'space-between';
leftPanel.style.alignItems = 'center';
leftPanel.style.width = '60px';
leftPanel.style.height = '100%';
leftPanel.style.zIndex = '10';
// Top container for zoom buttons.
const leftTop = document.createElement('div');
leftTop.style.display = 'flex';
leftTop.style.flexDirection = 'column';
leftTop.style.gap = '4px';
// Zoom In button.
const zoomInBtn = document.createElement('button');
zoomInBtn.className = 'material-icons zoom_in';
zoomInBtn.title = 'Zoom In';
zoomInBtn.style.background = 'transparent';
zoomInBtn.style.border = 'none';
zoomInBtn.style.cursor = 'pointer';
zoomInBtn.textContent = 'zoom_in';
// Zoom Out button.
const zoomOutBtn = document.createElement('button');
zoomOutBtn.className = 'material-icons zoom_out';
zoomOutBtn.title = 'Zoom Out';
zoomOutBtn.style.background = 'transparent';
zoomOutBtn.style.border = 'none';
zoomOutBtn.style.cursor = 'pointer';
zoomOutBtn.textContent = 'zoom_out';
leftTop.appendChild(zoomInBtn);
leftTop.appendChild(zoomOutBtn);
leftPanel.appendChild(leftTop);
// Bottom container for prev button.
const leftBottom = document.createElement('div');
leftBottom.style.display = 'flex';
leftBottom.style.justifyContent = 'center';
leftBottom.style.alignItems = 'center';
leftBottom.style.width = '100%';
if (images.length > 1) {
const prevBtn = document.createElement("button");
prevBtn.textContent = "";
prevBtn.className = "gallery-nav-btn";
prevBtn.style.background = 'transparent';
prevBtn.style.border = 'none';
prevBtn.style.color = 'white';
prevBtn.style.fontSize = '48px';
prevBtn.style.cursor = 'pointer';
prevBtn.addEventListener("click", function (e) {
e.stopPropagation();
// Safety check:
if (!modal.galleryImages || modal.galleryImages.length === 0) return;
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();
// Reset transforms.
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.transform = 'scale(1) rotate(0deg)';
});
leftBottom.appendChild(prevBtn);
} else {
// Insert an empty placeholder for consistent layout.
leftBottom.innerHTML = '&nbsp;';
}
leftPanel.appendChild(leftBottom);
// --- Center Panel: Contains the image ---
const centerPanel = document.createElement('div');
centerPanel.className = 'center-image-container';
centerPanel.style.flexGrow = '1';
centerPanel.style.textAlign = 'center';
centerPanel.style.position = 'relative';
centerPanel.style.zIndex = '1';
centerPanel.appendChild(img);
// --- Right Panel: Contains Rotate controls (top) and Next button (bottom) ---
const rightPanel = document.createElement('div');
rightPanel.className = 'right-panel';
rightPanel.style.display = 'flex';
rightPanel.style.flexDirection = 'column';
rightPanel.style.justifyContent = 'space-between';
rightPanel.style.alignItems = 'center';
rightPanel.style.width = '60px';
rightPanel.style.height = '100%';
rightPanel.style.zIndex = '10';
// Top container for rotate buttons.
const rightTop = document.createElement('div');
rightTop.style.display = 'flex';
rightTop.style.flexDirection = 'column';
rightTop.style.gap = '4px';
// Rotate Left button.
const rotateLeftBtn = document.createElement('button');
rotateLeftBtn.className = 'material-icons rotate_left';
rotateLeftBtn.title = 'Rotate Left';
rotateLeftBtn.style.background = 'transparent';
rotateLeftBtn.style.border = 'none';
rotateLeftBtn.style.cursor = 'pointer';
rotateLeftBtn.textContent = 'rotate_left';
// Rotate Right button.
const rotateRightBtn = document.createElement('button');
rotateRightBtn.className = 'material-icons rotate_right';
rotateRightBtn.title = 'Rotate Right';
rotateRightBtn.style.background = 'transparent';
rotateRightBtn.style.border = 'none';
rotateRightBtn.style.cursor = 'pointer';
rotateRightBtn.textContent = 'rotate_right';
rightTop.appendChild(rotateLeftBtn);
rightTop.appendChild(rotateRightBtn);
rightPanel.appendChild(rightTop);
// Bottom container for next button.
const rightBottom = document.createElement('div');
rightBottom.style.display = 'flex';
rightBottom.style.justifyContent = 'center';
rightBottom.style.alignItems = 'center';
rightBottom.style.width = '100%';
if (images.length > 1) {
const nextBtn = document.createElement("button");
nextBtn.textContent = "";
nextBtn.className = "gallery-nav-btn";
nextBtn.style.background = 'transparent';
nextBtn.style.border = 'none';
nextBtn.style.color = 'white';
nextBtn.style.fontSize = '48px';
nextBtn.style.cursor = 'pointer';
nextBtn.addEventListener("click", function (e) {
e.stopPropagation();
// Safety check:
if (!modal.galleryImages || modal.galleryImages.length === 0) return;
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();
// Reset transforms.
img.dataset.scale = 1;
img.dataset.rotate = 0;
img.style.transform = 'scale(1) rotate(0deg)';
});
rightBottom.appendChild(nextBtn);
} else {
// Insert a placeholder so that center remains properly aligned.
rightBottom.innerHTML = '&nbsp;';
}
rightPanel.appendChild(rightBottom);
// Assemble panels into the wrapper.
wrapper.appendChild(leftPanel);
wrapper.appendChild(centerPanel);
wrapper.appendChild(rightPanel);
container.appendChild(wrapper);
// --- Set up zoom controls event listeners ---
zoomInBtn.addEventListener('click', function (e) {
e.stopPropagation();
let scale = parseFloat(img.dataset.scale) || 1;
scale += 0.1;
img.dataset.scale = scale;
img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)';
});
zoomOutBtn.addEventListener('click', function (e) {
e.stopPropagation();
let scale = parseFloat(img.dataset.scale) || 1;
scale = Math.max(0.1, scale - 0.1);
img.dataset.scale = scale;
img.style.transform = 'scale(' + scale + ') rotate(' + img.dataset.rotate + 'deg)';
});
// Attach rotation control listeners (always present now).
rotateLeftBtn.addEventListener('click', function (e) {
e.stopPropagation();
let rotate = parseFloat(img.dataset.rotate) || 0;
rotate = (rotate - 90 + 360) % 360;
img.dataset.rotate = rotate;
img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)';
});
rotateRightBtn.addEventListener('click', function (e) {
e.stopPropagation();
let rotate = parseFloat(img.dataset.rotate) || 0;
rotate = (rotate + 90) % 360;
img.dataset.rotate = rotate;
img.style.transform = 'scale(' + img.dataset.scale + ') rotate(' + rotate + 'deg)';
});
// Save gallery details if there is more than one image.
if (images.length > 1) {
modal.galleryImages = images;
modal.galleryCurrentIndex = images.findIndex(f => f.name === fileName);
}
} else {
// Handle non-image file previews.
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|mkv|webm|mov|ogv)$/i.test(fileName)) {
const video = document.createElement("video");
video.src = fileUrl;
video.controls = true;
video.className = "image-modal-img";
const progressKey = 'videoProgress-' + fileUrl;
video.addEventListener("loadedmetadata", () => {
const savedTime = localStorage.getItem(progressKey);
if (savedTime) {
video.currentTime = parseFloat(savedTime);
}
});
video.addEventListener("timeupdate", () => {
localStorage.setItem(progressKey, video.currentTime);
});
video.addEventListener("ended", () => {
localStorage.removeItem(progressKey);
});
container.appendChild(video);
} else if (/\.(mp3|wav|m4a|ogg|flac|aac|wma|opus)$/i.test(fileName)) {
const audio = document.createElement("audio");
audio.src = fileUrl;
audio.controls = true;
audio.className = "audio-modal";
audio.style.maxWidth = "80vw";
container.appendChild(audio);
} else {
container.textContent = "Preview not available for this file type.";
}
}
modal.style.display = "flex";
}
// Preserve original functionality.
export function displayFilePreview(file, container) {
const actualFile = file.file || file;
if (!(actualFile instanceof File)) {
console.error("displayFilePreview called with an invalid file object");
return;
}
container.style.display = "inline-block";
while (container.firstChild) {
container.removeChild(container.firstChild);
}
if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico)$/i.test(actualFile.name)) {
const img = document.createElement("img");
img.src = URL.createObjectURL(actualFile);
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);
}
}
window.previewFile = previewFile;
window.openShareModal = openShareModal;