Files
SkyArtShop/website/admin/js/media-library.js

1132 lines
36 KiB
JavaScript
Raw Normal View History

2026-01-18 02:22:05 -06:00
/**
* Modern Media Library - Reusable Component
* Used across: Dashboard, Homepage, Products, Portfolio, Blog, Custom Pages, Settings
*/
class MediaLibrary {
constructor(options = {}) {
this.options = {
multiple: options.multiple || false,
onSelect: options.onSelect || null,
selectMode: options.selectMode || false,
containerId: options.containerId || "media-library-modal",
...options,
};
this.currentFolder = null;
this.folders = [];
this.files = [];
this.selectedItems = [];
this.viewMode = localStorage.getItem("mediaLibraryView") || "grid";
this.sortBy = localStorage.getItem("mediaLibrarySort") || "date";
this.searchQuery = "";
this.draggedItem = null;
this.modal = null;
this.initialized = false;
}
// Initialize the media library modal
init() {
if (this.initialized) return;
// Create modal HTML
this.createModal();
this.bindEvents();
this.initialized = true;
}
createModal() {
// Remove existing modal if any
const existing = document.getElementById(this.options.containerId);
if (existing) existing.remove();
const modalHtml = `
<div class="modal fade media-library-modal" id="${
this.options.containerId
}" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-xl modal-dialog-centered modal-fullscreen-lg-down">
<div class="modal-content">
<div class="modal-header media-library-header">
<div class="header-left">
<button type="button" class="btn btn-outline-light btn-sm back-btn" id="backBtn" style="display: none;">
<i class="bi bi-arrow-left"></i> Back
</button>
<div class="title-breadcrumb">
<h5 class="modal-title">
<i class="bi bi-images"></i>
Media Library
</h5>
<nav class="breadcrumb-nav" aria-label="breadcrumb">
<ol class="breadcrumb mb-0" id="folderBreadcrumb">
<li class="breadcrumb-item active">Root</li>
</ol>
</nav>
</div>
</div>
<div class="header-right">
<div class="view-toggle btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm ${
this.viewMode === "grid" ? "active" : ""
}" data-view="grid" title="Grid view">
<i class="bi bi-grid-3x3-gap-fill"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm ${
this.viewMode === "list" ? "active" : ""
}" data-view="list" title="List view">
<i class="bi bi-list-ul"></i>
</button>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body media-library-body">
<div class="media-library-toolbar">
<div class="toolbar-left">
<button type="button" class="btn btn-primary" id="uploadBtn">
<i class="bi bi-cloud-upload"></i> Upload
</button>
<button type="button" class="btn btn-outline-secondary" id="newFolderBtn">
<i class="bi bi-folder-plus"></i> New Folder
</button>
<div class="divider"></div>
<div class="search-box">
<i class="bi bi-search"></i>
<input type="text" placeholder="Search files..." id="mediaSearchInput">
</div>
</div>
<div class="toolbar-right">
<select class="form-select form-select-sm" id="sortSelect">
<option value="date" ${
this.sortBy === "date" ? "selected" : ""
}>Date (Newest)</option>
<option value="date-asc" ${
this.sortBy === "date-asc" ? "selected" : ""
}>Date (Oldest)</option>
<option value="name" ${
this.sortBy === "name" ? "selected" : ""
}>Name (A-Z)</option>
<option value="name-desc" ${
this.sortBy === "name-desc" ? "selected" : ""
}>Name (Z-A)</option>
<option value="size" ${
this.sortBy === "size" ? "selected" : ""
}>Size (Largest)</option>
<option value="size-asc" ${
this.sortBy === "size-asc" ? "selected" : ""
}>Size (Smallest)</option>
</select>
<span class="selected-count" id="selectedCount" style="display: none;">
<span class="count">0</span> selected
</span>
</div>
</div>
<div class="upload-drop-zone" id="uploadDropZone">
<div class="drop-zone-content">
<i class="bi bi-cloud-arrow-up"></i>
<p>Drag & drop files here or <span class="browse-link">browse</span></p>
<small>Supports: JPG, PNG, GIF, WebP, MP4, WebM, MOV (Max 100MB each)</small>
</div>
<input type="file" id="fileInput" multiple accept="image/*,video/*,.jpg,.jpeg,.png,.gif,.webp,.bmp,.tiff,.tif,.svg,.ico,.avif,.heic,.heif,.mp4,.webm,.mov,.avi,.mkv" hidden>
</div>
<div class="upload-progress" id="uploadProgress" style="display: none;">
<div class="progress-header">
<span>Uploading...</span>
<span class="progress-percent">0%</span>
</div>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
</div>
</div>
<div class="media-content ${
this.viewMode === "list" ? "list-view" : "grid-view"
}" id="mediaContent">
<div class="loading-spinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="modal-footer media-library-footer">
<div class="footer-left">
<button type="button" class="btn btn-outline-danger" id="deleteSelectedBtn" style="display: none;">
<i class="bi bi-trash"></i> Delete Selected
</button>
</div>
<div class="footer-right">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="selectFilesBtn" ${
this.options.selectMode ? "" : 'style="display: none;"'
}>
<i class="bi bi-check2"></i> Select${
this.options.multiple ? " Files" : " File"
}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Rename Modal -->
<div class="modal fade" id="renameModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-pencil"></i> Rename</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="renameInput" placeholder="Enter new name">
<input type="hidden" id="renameItemId">
<input type="hidden" id="renameItemType">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmRenameBtn">Rename</button>
</div>
</div>
</div>
</div>
<!-- Create Folder Modal -->
<div class="modal fade" id="createFolderModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-folder-plus"></i> Create New Folder</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="newFolderInput" placeholder="Folder name">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirmCreateFolderBtn">Create</button>
</div>
</div>
</div>
</div>
<!-- Image Preview Modal -->
<div class="image-preview-overlay" id="imagePreviewOverlay">
<button class="preview-close" id="previewClose">&times;</button>
<img src="" alt="Preview" id="previewImage">
<div class="preview-info">
<span class="preview-filename" id="previewFilename"></span>
<span class="preview-size" id="previewSize"></span>
</div>
</div>
`;
document.body.insertAdjacentHTML("beforeend", modalHtml);
this.modal = new bootstrap.Modal(
document.getElementById(this.options.containerId),
);
}
bindEvents() {
const container = document.getElementById(this.options.containerId);
// Back button
document.getElementById("backBtn").addEventListener("click", () => {
this.navigateBack();
});
// View toggle
container.querySelectorAll(".view-toggle button").forEach((btn) => {
btn.addEventListener("click", (e) => {
this.setViewMode(e.currentTarget.dataset.view);
});
});
// Sort change
document.getElementById("sortSelect").addEventListener("change", (e) => {
this.sortBy = e.target.value;
localStorage.setItem("mediaLibrarySort", this.sortBy);
this.renderContent();
});
// Search
document
.getElementById("mediaSearchInput")
.addEventListener("input", (e) => {
this.searchQuery = e.target.value.toLowerCase();
this.renderContent();
});
// Upload button
document.getElementById("uploadBtn").addEventListener("click", () => {
document.getElementById("fileInput").click();
});
// File input change
document.getElementById("fileInput").addEventListener("change", (e) => {
if (e.target.files.length > 0) {
this.uploadFiles(e.target.files);
}
});
// Drop zone
const dropZone = document.getElementById("uploadDropZone");
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", () => {
dropZone.classList.remove("dragover");
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
if (e.dataTransfer.files.length > 0) {
this.uploadFiles(e.dataTransfer.files);
}
});
dropZone.querySelector(".browse-link").addEventListener("click", () => {
document.getElementById("fileInput").click();
});
// New folder button
document.getElementById("newFolderBtn").addEventListener("click", () => {
document.getElementById("newFolderInput").value = "";
new bootstrap.Modal(document.getElementById("createFolderModal")).show();
});
// Create folder confirm
document
.getElementById("confirmCreateFolderBtn")
.addEventListener("click", () => {
this.createFolder();
});
// Enter key on folder input
document
.getElementById("newFolderInput")
.addEventListener("keypress", (e) => {
if (e.key === "Enter") this.createFolder();
});
// Rename confirm
document
.getElementById("confirmRenameBtn")
.addEventListener("click", () => {
this.confirmRename();
});
// Enter key on rename input
document.getElementById("renameInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") this.confirmRename();
});
// Delete selected
document
.getElementById("deleteSelectedBtn")
.addEventListener("click", () => {
this.deleteSelected();
});
// Select files button
document.getElementById("selectFilesBtn").addEventListener("click", () => {
this.confirmSelection();
});
// Image preview close
document.getElementById("previewClose").addEventListener("click", () => {
this.closePreview();
});
document
.getElementById("imagePreviewOverlay")
.addEventListener("click", (e) => {
if (e.target.id === "imagePreviewOverlay") this.closePreview();
});
// Escape key to close preview
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") this.closePreview();
});
}
setViewMode(mode) {
this.viewMode = mode;
localStorage.setItem("mediaLibraryView", mode);
const container = document.getElementById(this.options.containerId);
container.querySelectorAll(".view-toggle button").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.view === mode);
});
const content = document.getElementById("mediaContent");
content.classList.remove("grid-view", "list-view");
content.classList.add(mode === "list" ? "list-view" : "grid-view");
}
async open() {
this.init();
this.selectedItems = [];
this.currentFolder = null;
await this.loadContent();
this.modal.show();
this.updateSelectedCount();
}
close() {
if (this.modal) {
this.modal.hide();
}
}
async loadContent() {
try {
const [foldersRes, filesRes] = await Promise.all([
fetch("/api/admin/folders", { credentials: "include" }),
fetch(
`/api/admin/uploads${
this.currentFolder
? `?folder_id=${this.currentFolder}`
: "?folder_id=null"
}`,
{ credentials: "include" },
),
]);
const foldersData = await foldersRes.json();
const filesData = await filesRes.json();
this.folders = foldersData.folders || [];
this.files = filesData.files || [];
this.renderContent();
this.updateBreadcrumb();
} catch (error) {
console.error("Error loading media:", error);
this.showNotification("Failed to load media library", "error");
}
}
renderContent() {
const content = document.getElementById("mediaContent");
// Filter by search
let filteredFolders = this.folders.filter((f) =>
this.currentFolder
? f.parentId === this.currentFolder
: f.parentId === null,
);
let filteredFiles = this.files;
if (this.searchQuery) {
filteredFolders = filteredFolders.filter((f) =>
f.name.toLowerCase().includes(this.searchQuery),
);
filteredFiles = filteredFiles.filter(
(f) =>
f.originalName.toLowerCase().includes(this.searchQuery) ||
f.filename.toLowerCase().includes(this.searchQuery),
);
}
// Sort files
filteredFiles = this.sortFiles(filteredFiles);
if (filteredFolders.length === 0 && filteredFiles.length === 0) {
content.innerHTML = `
<div class="empty-state">
<i class="bi bi-folder2-open"></i>
<h5>${
this.searchQuery ? "No results found" : "This folder is empty"
}</h5>
<p>${
this.searchQuery
? "Try a different search term"
: "Upload files or create a folder to get started"
}</p>
</div>
`;
return;
}
let html = "";
// Render folders first
filteredFolders.forEach((folder) => {
const isSelected = this.selectedItems.some(
(s) => s.type === "folder" && s.id === folder.id,
);
html += `
<div class="media-item folder-item ${isSelected ? "selected" : ""}"
data-type="folder" data-id="${folder.id}" data-name="${
folder.name
}"
draggable="true">
<div class="item-checkbox">
<input type="checkbox" ${isSelected ? "checked" : ""}>
</div>
<div class="item-icon">
<i class="bi bi-folder-fill"></i>
</div>
<div class="item-info">
<span class="item-name">${this.escapeHtml(folder.name)}</span>
<span class="item-meta">${folder.fileCount || 0} files</span>
</div>
<div class="item-actions">
<button class="btn-action" data-action="rename" title="Rename">
<i class="bi bi-pencil"></i>
</button>
<button class="btn-action btn-danger" data-action="delete" title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
});
// Render files
filteredFiles.forEach((file) => {
const isSelected = this.selectedItems.some(
(s) => s.type === "file" && s.id === file.id,
);
const fileSize = this.formatFileSize(file.size);
const isVideo = this.isVideoFile(file.originalName || file.path);
const previewHtml = isVideo
? `<div class="video-preview-placeholder"><i class="bi bi-play-circle-fill"></i><span>Video</span></div>`
: `<img src="${file.path}" alt="${file.originalName}" loading="lazy">`;
html += `
<div class="media-item file-item ${isSelected ? "selected" : ""}"
data-type="file" data-id="${file.id}" data-path="${file.path}"
data-name="${file.originalName}" data-size="${file.size}"
data-is-video="${isVideo}"
draggable="true">
<div class="item-checkbox">
<input type="checkbox" ${isSelected ? "checked" : ""}>
</div>
<div class="item-preview">
${previewHtml}
</div>
<div class="item-info">
<span class="item-name" title="${this.escapeHtml(
file.originalName,
)}">${this.escapeHtml(file.originalName)}</span>
<span class="item-meta">${fileSize}</span>
</div>
<div class="item-actions">
<button class="btn-action" data-action="preview" title="Preview">
<i class="bi bi-eye"></i>
</button>
<button class="btn-action" data-action="rename" title="Rename">
<i class="bi bi-pencil"></i>
</button>
<button class="btn-action btn-danger" data-action="delete" title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
});
content.innerHTML = html;
this.bindItemEvents();
}
bindItemEvents() {
const content = document.getElementById("mediaContent");
content.querySelectorAll(".media-item").forEach((item) => {
// Click to select
item.addEventListener("click", (e) => {
if (
e.target.closest(".item-actions") ||
e.target.closest(".item-checkbox")
)
return;
const type = item.dataset.type;
const id = parseInt(item.dataset.id);
// Double click to open folder or preview image
if (type === "folder") {
this.currentFolder = id;
this.loadContent();
return;
}
// Single click to select/deselect
this.toggleSelection(item);
});
// Checkbox click
item
.querySelector(".item-checkbox input")
.addEventListener("change", (e) => {
e.stopPropagation();
this.toggleSelection(item);
});
// Action buttons
item.querySelectorAll(".btn-action").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const action = btn.dataset.action;
const type = item.dataset.type;
const id = parseInt(item.dataset.id);
const name = item.dataset.name;
switch (action) {
case "preview":
this.showPreview(item.dataset.path, name, item.dataset.size);
break;
case "rename":
this.showRenameModal(id, type, name);
break;
case "delete":
this.deleteItem(id, type, name);
break;
}
});
});
// Drag and drop
item.addEventListener("dragstart", (e) => {
this.draggedItem = {
type: item.dataset.type,
id: parseInt(item.dataset.id),
};
item.classList.add("dragging");
});
item.addEventListener("dragend", () => {
item.classList.remove("dragging");
this.draggedItem = null;
});
if (item.dataset.type === "folder") {
item.addEventListener("dragover", (e) => {
e.preventDefault();
if (this.draggedItem && this.draggedItem.type === "file") {
item.classList.add("drag-over");
}
});
item.addEventListener("dragleave", () => {
item.classList.remove("drag-over");
});
item.addEventListener("drop", (e) => {
e.preventDefault();
item.classList.remove("drag-over");
if (this.draggedItem && this.draggedItem.type === "file") {
this.moveFileToFolder(
this.draggedItem.id,
parseInt(item.dataset.id),
);
}
});
}
});
}
toggleSelection(item) {
const type = item.dataset.type;
const id = parseInt(item.dataset.id);
const path = item.dataset.path;
const name = item.dataset.name;
// Only allow selecting files in select mode
if (this.options.selectMode && type === "folder") {
// Navigate into folder instead
this.currentFolder = id;
this.loadContent();
return;
}
const existingIndex = this.selectedItems.findIndex(
(s) => s.type === type && s.id === id,
);
if (existingIndex >= 0) {
this.selectedItems.splice(existingIndex, 1);
item.classList.remove("selected");
item.querySelector(".item-checkbox input").checked = false;
} else {
if (!this.options.multiple && this.options.selectMode) {
// Single select mode - clear previous selection
this.selectedItems = [];
document.querySelectorAll(".media-item.selected").forEach((el) => {
el.classList.remove("selected");
el.querySelector(".item-checkbox input").checked = false;
});
}
this.selectedItems.push({ type, id, path, name });
item.classList.add("selected");
item.querySelector(".item-checkbox input").checked = true;
}
this.updateSelectedCount();
}
updateSelectedCount() {
const count = this.selectedItems.filter((s) => s.type === "file").length;
const countEl = document.getElementById("selectedCount");
const deleteBtn = document.getElementById("deleteSelectedBtn");
const selectBtn = document.getElementById("selectFilesBtn");
if (count > 0) {
countEl.style.display = "inline-flex";
countEl.querySelector(".count").textContent = count;
deleteBtn.style.display = "inline-flex";
if (this.options.selectMode) {
selectBtn.disabled = false;
}
} else {
countEl.style.display = "none";
deleteBtn.style.display = "none";
if (this.options.selectMode) {
selectBtn.disabled = true;
}
}
}
sortFiles(files) {
const sorted = [...files];
switch (this.sortBy) {
case "date":
sorted.sort((a, b) => new Date(b.uploadDate) - new Date(a.uploadDate));
break;
case "date-asc":
sorted.sort((a, b) => new Date(a.uploadDate) - new Date(b.uploadDate));
break;
case "name":
sorted.sort((a, b) => a.originalName.localeCompare(b.originalName));
break;
case "name-desc":
sorted.sort((a, b) => b.originalName.localeCompare(a.originalName));
break;
case "size":
sorted.sort((a, b) => b.size - a.size);
break;
case "size-asc":
sorted.sort((a, b) => a.size - b.size);
break;
}
return sorted;
}
updateBreadcrumb() {
const breadcrumb = document.getElementById("folderBreadcrumb");
const backBtn = document.getElementById("backBtn");
let html = `<li class="breadcrumb-item"><a href="#" data-folder="null">Root</a></li>`;
// Show/hide back button based on current folder
if (this.currentFolder) {
backBtn.style.display = "inline-flex";
} else {
backBtn.style.display = "none";
}
if (this.currentFolder) {
const folder = this.folders.find((f) => f.id === this.currentFolder);
if (folder) {
const pathParts = folder.path.split("/").filter((p) => p);
let currentPath = "";
pathParts.forEach((part, index) => {
currentPath += "/" + part;
const pathFolder = this.folders.find((f) => f.path === currentPath);
const isLast = index === pathParts.length - 1;
if (isLast) {
html += `<li class="breadcrumb-item active">${this.escapeHtml(
part,
)}</li>`;
} else if (pathFolder) {
html += `<li class="breadcrumb-item"><a href="#" data-folder="${
pathFolder.id
}">${this.escapeHtml(part)}</a></li>`;
}
});
}
}
breadcrumb.innerHTML = html;
// Bind breadcrumb clicks
breadcrumb.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const folderId = link.dataset.folder;
this.currentFolder = folderId === "null" ? null : parseInt(folderId);
this.loadContent();
});
});
}
navigateBack() {
if (!this.currentFolder) return;
// Find the parent folder
const currentFolderObj = this.folders.find(
(f) => f.id === this.currentFolder,
);
if (currentFolderObj && currentFolderObj.parentId) {
this.currentFolder = currentFolderObj.parentId;
} else {
this.currentFolder = null;
}
this.loadContent();
}
async uploadFiles(fileList) {
const files = Array.from(fileList);
const progressEl = document.getElementById("uploadProgress");
const progressBar = progressEl.querySelector(".progress-bar");
const progressPercent = progressEl.querySelector(".progress-percent");
progressEl.style.display = "block";
progressBar.style.width = "0%";
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
if (this.currentFolder) {
formData.append("folder_id", this.currentFolder);
}
try {
const response = await fetch("/api/admin/upload", {
method: "POST",
body: formData,
credentials: "include",
});
const data = await response.json();
if (data.success) {
progressBar.style.width = "100%";
progressPercent.textContent = "100%";
this.showNotification(
`${data.files.length} file(s) uploaded successfully`,
"success",
);
await this.loadContent();
} else {
throw new Error(data.message || "Upload failed");
}
} catch (error) {
console.error("Upload error:", error);
this.showNotification(error.message || "Upload failed", "error");
} finally {
setTimeout(() => {
progressEl.style.display = "none";
progressBar.style.width = "0%";
}, 1000);
document.getElementById("fileInput").value = "";
}
}
async createFolder() {
const input = document.getElementById("newFolderInput");
const name = input.value.trim();
if (!name) {
this.showNotification("Please enter a folder name", "error");
return;
}
try {
const response = await fetch("/api/admin/folders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: name,
parent_id: this.currentFolder,
}),
credentials: "include",
});
const data = await response.json();
if (data.success) {
bootstrap.Modal.getInstance(
document.getElementById("createFolderModal"),
).hide();
this.showNotification("Folder created successfully", "success");
await this.loadContent();
} else {
throw new Error(data.error || "Failed to create folder");
}
} catch (error) {
console.error("Create folder error:", error);
this.showNotification(
error.message || "Failed to create folder",
"error",
);
}
}
showRenameModal(id, type, currentName) {
document.getElementById("renameItemId").value = id;
document.getElementById("renameItemType").value = type;
// Remove extension for files
let displayName = currentName;
if (type === "file") {
const lastDot = currentName.lastIndexOf(".");
if (lastDot > 0) {
displayName = currentName.substring(0, lastDot);
}
}
document.getElementById("renameInput").value = displayName;
new bootstrap.Modal(document.getElementById("renameModal")).show();
setTimeout(() => {
document.getElementById("renameInput").select();
}, 100);
}
async confirmRename() {
const id = parseInt(document.getElementById("renameItemId").value);
const type = document.getElementById("renameItemType").value;
const newName = document.getElementById("renameInput").value.trim();
if (!newName) {
this.showNotification("Please enter a name", "error");
return;
}
try {
const endpoint =
type === "folder"
? `/api/admin/folders/${id}/rename`
: `/api/admin/uploads/${id}/rename`;
const response = await fetch(endpoint, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newName }),
credentials: "include",
});
const data = await response.json();
if (data.success) {
bootstrap.Modal.getInstance(
document.getElementById("renameModal"),
).hide();
this.showNotification("Renamed successfully", "success");
await this.loadContent();
} else {
throw new Error(data.error || "Failed to rename");
}
} catch (error) {
console.error("Rename error:", error);
this.showNotification(error.message || "Failed to rename", "error");
}
}
async deleteItem(id, type, name) {
const self = this;
showDeleteConfirm(
`Are you sure you want to delete "${name}"?`,
async () => {
try {
const endpoint =
type === "folder"
? `/api/admin/folders/${id}?delete_contents=true`
: `/api/admin/uploads/id/${id}`;
const response = await fetch(endpoint, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
self.showNotification("Deleted successfully", "success");
// Remove from selected items
self.selectedItems = self.selectedItems.filter(
(s) => !(s.type === type && s.id === id),
);
self.updateSelectedCount();
await self.loadContent();
} else {
throw new Error(data.error || "Failed to delete");
}
} catch (error) {
console.error("Delete error:", error);
self.showNotification(error.message || "Failed to delete", "error");
}
},
{ title: "Delete Item", confirmText: "Delete" },
);
}
async deleteSelected() {
const fileIds = this.selectedItems
.filter((s) => s.type === "file")
.map((s) => s.id);
const folderIds = this.selectedItems
.filter((s) => s.type === "folder")
.map((s) => s.id);
if (fileIds.length === 0 && folderIds.length === 0) return;
const total = fileIds.length + folderIds.length;
const self = this;
showDeleteConfirm(
`Are you sure you want to delete ${total} item(s)?`,
async () => {
try {
// Delete folders first
for (const folderId of folderIds) {
await fetch(`/api/admin/folders/${folderId}?delete_contents=true`, {
method: "DELETE",
credentials: "include",
});
}
// Delete files
if (fileIds.length > 0) {
await fetch("/api/admin/uploads/bulk-delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ file_ids: fileIds }),
credentials: "include",
});
}
self.showNotification(`${total} item(s) deleted`, "success");
self.selectedItems = [];
self.updateSelectedCount();
await self.loadContent();
} catch (error) {
console.error("Bulk delete error:", error);
self.showNotification("Failed to delete some items", "error");
}
},
{ title: "Delete Items", confirmText: `Delete ${total} Items` },
);
}
async moveFileToFolder(fileId, folderId) {
try {
const response = await fetch("/api/admin/uploads/move", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
file_ids: [fileId],
folder_id: folderId,
}),
credentials: "include",
});
const data = await response.json();
if (data.success) {
this.showNotification("File moved successfully", "success");
await this.loadContent();
} else {
throw new Error(data.error || "Failed to move file");
}
} catch (error) {
console.error("Move error:", error);
this.showNotification(error.message || "Failed to move file", "error");
}
}
confirmSelection() {
const selectedFiles = this.selectedItems.filter((s) => s.type === "file");
if (selectedFiles.length === 0) {
this.showNotification("Please select at least one file", "error");
return;
}
if (this.options.onSelect) {
const result = this.options.multiple ? selectedFiles : selectedFiles[0];
this.options.onSelect(result);
}
this.close();
}
showPreview(path, name, size) {
const overlay = document.getElementById("imagePreviewOverlay");
document.getElementById("previewImage").src = path;
document.getElementById("previewFilename").textContent = name;
document.getElementById("previewSize").textContent = this.formatFileSize(
parseInt(size),
);
overlay.classList.add("active");
document.body.style.overflow = "hidden";
}
closePreview() {
const overlay = document.getElementById("imagePreviewOverlay");
overlay.classList.remove("active");
document.body.style.overflow = "";
}
showNotification(message, type = "info") {
// Create toast notification
const toast = document.createElement("div");
toast.className = `media-toast ${type}`;
toast.innerHTML = `
<i class="bi bi-${
type === "success"
? "check-circle"
: type === "error"
? "exclamation-circle"
: "info-circle"
}"></i>
<span>${this.escapeHtml(message)}</span>
`;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add("show"), 10);
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 3000);
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
isVideoFile(filename) {
if (!filename) return false;
const ext = filename.toLowerCase().split(".").pop();
return ["mp4", "webm", "mov", "avi", "mkv"].includes(ext);
}
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
}
// Export for use in other files
window.MediaLibrary = MediaLibrary;