UNPKG

@astsiry/media-manager-ui

Version:

Private media manager UI for internal CDN/FTP use

1,093 lines (1,091 loc) 45.8 kB
function isFolderNode(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } export class MediaManager { constructor({ containerId = "", basePath = "/", btnText = "Toogle media manager", btnId = "mediaToggle", API_URL = "http://192.168.0.121:8000", uiText, }) { this.isReady = false; this.searchTerm = ""; this.headerTitle = ""; // data-uiText="" // ${this.uiText.} this.uiText = { headerTitle: "Media Manager", defaultLoadingText: "Loading folder structure", noFolder: "No folder", noFile: "No file", noFileSelected: "No file selected", noFileFound: "No matching files found", noFileInFolder: "No files in this folder", dragAndDropText: "or drag & drop files here", upload: "Upload", searchPlaceholder: "Search files...", loadingFolderStructure: "Loading folder structure", newFolderTitle: "Enter new folder name:", loadingFolderCreation: "Creating folder", notifFolderCreateSuccess: "Folder created successfully", notifFolderCreateError: "Failed to create folder", failed: "Failed", delete: "Delete", deleteConfirmationText: "Are you sure ? <br>All instances that use this file will become unavailable.", copied: "Copied!", uploadSuccess: "Upload success", uploadFailed: "Upload failed.", deleteSuccess: "File deleted", deleteFailed: "Failed to delete file.", loadingUpload: "Uploading file", loadingDelete: "Delleting file", newFolder: "New Folder", home: "Home", imgCopied: "Image tag copied!", imgCopyBtnText: "Image tag", urlCopied: "URL copied!", urlCopyBtnText: "Copy URL", download: "Download", confirmBtnText: "Yes", cancelBtnText: "No", promptBtnText: "OK", cancelPropmtBtnText: "Cancel", }; if (uiText) { this.uiText = { ...this.uiText, ...uiText }; } this.API_URL = API_URL; this.openFolders = new Set(); this.container = document.getElementById(containerId); this.basePath = basePath; this.btnText = btnText; this.btnId = btnId; this.folderTree = { "/": {}, }; this.currentPath = `/`; this.selectedFile = null; this._renderStructure(); this._bindEvents(); this._init(); } async initCdnUrl() { this.cdnUrl = await (await fetch(`${this.API_URL}/media/cdn_url`)).json(); if (this.basePath && this.basePath !== "/") { this.cdnUrl = this.joinUrlParts(this.cdnUrl, this.basePath); } } async _init() { await this.initCdnUrl(); await this._fetchFolderTree(); // const rootData = await this._fetchFolderContent(this.currentPath); // if (rootData) { // const rootNode = this.folderTree["/"]; // rootNode.files = rootData.files; // for (const f of rootData.folders) { // rootNode[f.name] = {}; // if (f.hasContent) rootNode[f.name]._hasContent = true; // } // } // console.log("Initial folder structure:", this.folderTree); this._renderFolders(); this._renderThumbnails(); this._renderPreview(); this._renderImageViewerModal(); this.isReady = true; this._toggleLoader(false); } getDefaultUIText() { return this.uiText; } _renderStructure() { if (!this.container) return; this.container.insertAdjacentHTML("afterbegin", ` <button class="media-btn" id="${this.btnId}"> <div class="loading-in-btn ant-spinner-wrapper"> <div class="ant-spinner-dot ant-dot-1"></div> <div class="ant-spinner-dot ant-dot-2"></div> <div class="ant-spinner-dot ant-dot-3"></div> <div class="ant-spinner-dot ant-dot-4"></div> </div> ${this.btnText} </button> `); this.container.insertAdjacentHTML("beforeend", ` <div class="modal-backdrop" id="modalBackdrop"></div> <div class="media-modal" id="mediaModal"> <div class="modal-header" data-uiText="headerTitle"> <h3><i class="bi bi-images"></i> ${this.uiText.headerTitle}</h3> <span class="modal-close" id="modalClose">&times;</span> </div> <div class="modal-body"> <div id="media-loader" class="hide-loader"> <div class="ant-spinner-wrapper"> <div class="ant-spinner-dot ant-dot-1"></div> <div class="ant-spinner-dot ant-dot-2"></div> <div class="ant-spinner-dot ant-dot-3"></div> <div class="ant-spinner-dot ant-dot-4"></div> </div> <div class="loader-text"> <span class="msg" data-uiText="defaultLoadingText">${this.uiText.defaultLoadingText}</span> <span class="dot">.</span><span class="dot">.</span><span class="dot">.</span> </div> </div> <div class="breadcrumbs" id="breadcrumbTrail"></div> <div class="columns-wrapper"> <div class="col folder-tree" id="folderCol"></div> <div class="col" id="previewCol"> <div class="thumb-grid" id="thumbGrid"> <div class="empty-preview"> <i class="bi bi-folder-x"></i> <div data-uiText="noFolder">${this.uiText.noFolder}</div> </div> </div> <div class="drop-zone" id="dropZone"> <div class="upload-wrapper"> <div class="upload-list"></div> </div> <input type="file" accept="*" multiple hidden id="uploadInput" style="display:none"> <button class="upload-btn" id="uploadBtn" data-uiText="upload">${this.uiText.upload}</button> <p id="uploadText" data-uiText="dragAndDropText">${this.uiText.dragAndDropText}</p> </div> </div> <div class="col preview" id="imagePreview"> <div class="preview-empty"> <i class="bi bi-file-earmark-x"></i> <div data-uiText="noFile"> ${this.uiText.noFile}</div> </div> </div> </div> </div> </div> `); this.modal = this.container.querySelector("#mediaModal"); this.backdrop = this.container.querySelector("#modalBackdrop"); this.folderCol = this.container.querySelector("#folderCol"); this.thumbGrid = this.container.querySelector("#thumbGrid"); this.previewCol = this.container.querySelector("#imagePreview"); this.uploadBtn = this.container.querySelector("#uploadBtn"); this.uploadInput = this.container.querySelector("#uploadInput"); this.breadcrumbTrail = this.container.querySelector("#breadcrumbTrail"); this.toggleBtn = document.getElementById(this.btnId); // this.themeBtn = this.container.querySelector("#themeToggle"); this.closeBtn = this.container.querySelector("#modalClose"); // Insert search UI just after initializing `this.thumbGrid` const searchWrapper = document.createElement("div"); searchWrapper.className = "thumb-search-wrapper"; this.searchInput = document.createElement("input"); this.searchInput.className = "thumb-search-input"; this.searchInput.type = "search"; this.searchInput.dataset.uiText = "searchPlaceholder"; this.searchInput.placeholder = this.uiText.searchPlaceholder; const clearBtn = document.createElement("i"); clearBtn.className = "bi bi-x-lg clear-search hidden"; const searchIcon = document.createElement("i"); searchIcon.className = "bi bi-search trigger-search"; // Bind search behavior const updateClearBtn = () => { clearBtn.classList.toggle("hidden", !this.searchInput.value.trim()); }; const applySearch = () => { this.searchTerm = this.searchInput.value.trim(); this._renderThumbnails(); }; this.searchInput.oninput = updateClearBtn; this.searchInput.onkeydown = (e) => { if (e.key === "Enter") applySearch(); }; this.searchInput.onblur = applySearch; searchIcon.onclick = applySearch; clearBtn.onclick = () => { this.searchInput.value = ""; this.searchTerm = ""; updateClearBtn(); this._renderThumbnails(); }; searchWrapper.append(this.searchInput, searchIcon, clearBtn); this.thumbGrid.insertAdjacentElement("beforebegin", searchWrapper); } _bindEvents() { this.toggleBtn.onclick = () => { this.modal.classList.add("show"); this.backdrop.classList.add("show"); if (!this.isReady) { this._toggleLoader(true, this.uiText.loadingFolderStructure); } }; this.closeBtn.onclick = () => { this.modal.classList.remove("show"); this.backdrop.classList.remove("show"); }; // Upload btn this.uploadBtn.onclick = () => this.uploadInput.click(); this.uploadInput.onchange = async (e) => { const files = e.target.files; await this._handleFilesUpload(files); }; if (!this.container) return; // DragAndDrop this.dropZone = this.container.querySelector("#dropZone"); this.dropZone.addEventListener("dragover", (e) => { e.preventDefault(); this.dropZone.classList.add("dragover"); }); this.dropZone.addEventListener("dragleave", () => { this.dropZone.classList.remove("dragover"); }); this.dropZone.addEventListener("drop", async (e) => { e.preventDefault(); e.stopPropagation(); const files = e.dataTransfer.files; await this._handleFilesUpload(files); this.dropZone.classList.remove("dragover"); }); // Theme toggle // this.themeBtn.onclick = () => { // document.body.classList.toggle("dark-mode"); // document.body.classList.toggle("light-mode"); // this.themeBtn.textContent = document.body.classList.contains("light-mode") // ? "Switch to Night Mode" // : "Switch to Day Mode"; // }; } _toggleLoader(isShow = true, msg = "Loading") { if (!this.container) return; const loader = this.container.querySelector("#media-loader"); if (!loader) return; const msgSpan = loader.querySelector(".loader-text .msg"); if (!msgSpan) return; msgSpan.textContent = msg; if (isShow) { loader.classList.add("show-loader"); loader.classList.remove("hide-loader"); } else { loader.classList.remove("show-loader"); loader.classList.add("hide-loader"); this.container.querySelector(".loading-in-btn")?.classList.add("hidden"); } } _expandToPath(path) { if (!this.openFolders) this.openFolders = new Set(); const parts = path.split("/").filter(Boolean); let accumulated = "/"; for (let i = 0; i < parts.length; i++) { accumulated += parts[i] + "/"; this.openFolders.add(accumulated.replace(/\/\//g, "/").replace(/\/\//g, "/")); } } async _createFolder() { // const folderName = prompt("Enter new folder name:"); const folderName = await this._customPrompt(this.uiText.newFolderTitle || ""); if (!folderName) return; this._toggleLoader(true, this.uiText.loadingFolderCreation); const newPath = this.currentPath + folderName + "/"; const sanitizedPath = newPath.replace(/\/\//g, "/"); const res = await fetch(`${this.API_URL}/media/folder`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: (this.basePath + "/" + sanitizedPath) .replace(/\/\//g, "/") .replace(/\/\//g, "/"), }), }); if (res.ok) { this._ensureFolderPath(sanitizedPath); this.currentPath = sanitizedPath; this._expandToPath(sanitizedPath); this.selectedFile = null; this._renderFolders(); this._renderThumbnails(); this._renderPreview(); this._notify(this.uiText.notifFolderCreateSuccess || "", "success"); } else { this._notify(this.uiText.notifFolderCreateError || "", "error"); } this._toggleLoader(false); } async _handleFilesUpload(files) { if (!files || !files.length || !this.container) return; const uploadBtn = this.container.querySelector("#uploadBtn"); const uploadText = this.container.querySelector("#uploadText"); if (!uploadBtn || !uploadText) return; uploadBtn.classList.add("hidden"); uploadText.classList.add("hidden"); const uploadWrapper = this.dropZone.querySelector(".upload-wrapper"); const uploadZone = this.dropZone.querySelector(".upload-list"); uploadWrapper.classList.add("show-upload"); uploadZone.innerHTML = ""; // clear old content const formUploads = Array.from(files); const placeholders = new Map(); // Step 1: create all placeholders first for (const file of formUploads) { const placeholder = document.createElement("div"); placeholder.className = "upload-skeleton"; placeholder.innerHTML = ` <div class="file-info">${file.name}</div> <div class="progress-bar"><div class="bar"></div></div> `; uploadZone.appendChild(placeholder); placeholders.set(file.name, placeholder); } // Step 2: perform all uploads in parallel const uploads = formUploads.map((file) => { const formData = new FormData(); formData.append("file", file); formData.append("folder", (this.basePath + "/" + this.currentPath) .replace(/\/\//g, "/") .replace(/\/\//g, "/")); return fetch(`${this.API_URL}/media/upload`, { method: "POST", body: formData, }) .then(async (res) => { if (!res.ok) throw new Error("Upload failed"); const { filename } = await res.json(); this._addFile(this.currentPath, filename); const isImage = this._isImage(filename); const result = document.createElement("div"); result.className = "upload-result"; result.innerHTML = isImage ? `<img src="${this.cdnUrl}/${this.currentPath}${filename}" />` : `<i class="bi ${this._getFileTypeIcon(filename)} file-icon"></i>`; const check = document.createElement("i"); check.className = "bi bi-check-circle-fill success-icon"; result.appendChild(check); const skeleton = placeholders.get(file.name); if (skeleton) { uploadZone.replaceChild(result, skeleton); } this._renderThumbnails(); }) .catch(() => { const skeleton = placeholders.get(file.name); if (skeleton) { skeleton.classList.add("error"); skeleton.innerHTML += `<div class="error-text">${this.uiText.failed}</div>`; } }); }); // Step 3: Wait for all to settle, then hide loader Promise.allSettled(uploads).then(() => { this._notify(this.uiText.uploadSuccess ?? "", "success"); setTimeout(() => { uploadWrapper.classList.remove("show-upload"); uploadBtn.classList.remove("hidden"); uploadText.classList.remove("hidden"); setTimeout(() => { uploadZone.innerHTML = ""; }, 1000); }, 1500); }); } async _uploadFile(file) { this._toggleLoader(true, this.uiText.loadingUpload); const formData = new FormData(); formData.append("file", file); formData.append("folder", (this.basePath + "/" + this.currentPath) .replace(/\/\//g, "/") .replace(/\/\//g, "/")); const response = await fetch(`${this.API_URL}/media/upload`, { method: "POST", body: formData, }); if (response.ok) { const r = await response.json(); // this._getFiles(this.currentPath).push(r.filename); this._addFile(this.currentPath, r.filename); this.selectedFile = r.filename; this._renderThumbnails(); } else { alert(this.uiText.uploadFailed); } this._toggleLoader(false); } _ensureFolderPath(path) { const parts = path .replace(/^\/|\/$/g, "") .split("/") .filter(Boolean); let node = this.folderTree["/"]; for (const part of parts) { if (!node[part]) { node[part] = {}; } node = node[part]; } if (!node.files) { node.files = []; } return node; } _getFolderNode(path) { const parts = path .replace(/^\/|\/$/g, "") .split("/") .filter(Boolean); let node = this.folderTree["/"]; for (const part of parts) { if (!node[part]) return null; node = node[part]; } return node; } _addFile(path, filename) { const node = this._ensureFolderPath(path); if (!node.files.includes(filename)) { node.files.push(filename); } } _deleteFile(path, filename) { const node = this._ensureFolderPath(path); if (node.files) { node.files = node.files.filter((f) => f !== filename); } } _getFiles(path) { const node = this._getFolderNode(path); return (node?.files?.slice().sort((a, b) => { const aNum = parseInt(a.split("-")[0], 10); const bNum = parseInt(b.split("-")[0], 10); return bNum - aNum; }) || []); } _renderFolders(tree = null) { this.folderCol.innerHTML = ""; const newFolderBtn = document.createElement("button"); newFolderBtn.innerHTML = `<i class="bi bi-folder-plus"></i> ${this.uiText.newFolder}`; newFolderBtn.dataset.uiText = `newFolder`; newFolderBtn.className = "upload-btn"; newFolderBtn.onclick = () => this._createFolder(); this.folderCol.prepend(newFolderBtn); // 🏠 Home Folder Button const homeDiv = document.createElement("div"); homeDiv.className = "folder-label home-folder"; homeDiv.innerHTML = `<i class="bi bi-house-door-fill"></i> <span data-uiText="home">${this.uiText.home}</span>`; homeDiv.onclick = () => { this.currentPath = "/"; this.selectedFile = null; this._renderFolders(); this._renderThumbnails(); this._renderPreview(); this._renderBreadcrumbs(); }; // Highlight if selected if (this.currentPath === "/") homeDiv.classList.add("active"); this.folderCol.appendChild(homeDiv); if (!tree) { tree = this._buildTree(this.folderTree["/"], "/"); } this.folderCol.appendChild(tree); this._renderBreadcrumbs(); } _buildTree(node, path) { const ul = document.createElement("ul"); ul.className = "folder-tree-parrent"; Object.entries(node).forEach(([key, value]) => { if (key === "files" || key === "_hasContent") return; const li = document.createElement("li"); const folderPath = `/${path}${key}/`.replace(/\/\//g, "/"); const label = document.createElement("div"); label.className = "folder-label"; label.dataset.path = folderPath; // const hasChildren = // value._hasContent || // Object.keys(value).some((k) => k !== "files" && k !== "_hasContent"); const hasChildren = typeof value === "object" && value !== null && ("_hasContent" in value || Object.keys(value).some((k) => k !== "files" && k !== "_hasContent")); const shouldShow = this.openFolders.has(folderPath) || this.currentPath === folderPath; const caret = document.createElement("i"); caret.className = hasChildren ? "caret bi bi-caret-right-fill" : "caret"; const icon = document.createElement("i"); icon.className = "bi bi-folder"; const text = document.createElement("span"); text.textContent = key; label.append(caret, icon, text); li.appendChild(label); let children; if (hasChildren || shouldShow) { if (isFolderNode(value)) { children = this._buildTree(value, folderPath); children.style.display = shouldShow ? "block" : "none"; if (shouldShow) caret.classList.add("rotate"); li.appendChild(children); } } label.onclick = async () => { // this._toggleLoader(true, `Loading folder: ${key}`); const newPath = `/${path}${key}/`.replace(/\/\//g, "/"); this.currentPath = newPath; this.selectedFile = null; // const folderNode = this._ensureFolderPath(newPath); // const alreadyLoaded = !("_hasContent" in folderNode); // if (!alreadyLoaded) { // const content = await this._fetchFolderContent(newPath); // if (content) { // folderNode.files = content.files || []; // for (const f of content.folders || []) { // if (!folderNode[f.name]) folderNode[f.name] = {}; // if (f.hasContent) folderNode[f.name]._hasContent = true; // } // delete folderNode._hasContent; // } // } // const newTree = this._buildTree(folderNode, path); // console.log('New tree:', newTree ); // label.nextElementSibling?.replaceWith(newTree); if (hasChildren) { const open = caret.classList.toggle("rotate"); children.style.display = open ? "block" : "none"; const folderPath = `/${path}${key}/`.replace(/\/\//g, "/"); if (open) { this.openFolders.add(folderPath); } else { this.openFolders.delete(folderPath); } } this._renderThumbnails(); this._renderPreview(); this._renderBreadcrumbs(); if (!this.container) return; this.container.querySelectorAll(".active-folder").forEach((el) => { el.classList.remove("active-folder"); }); label.classList.add("active-folder"); // this._toggleLoader(false); }; if (this.currentPath === folderPath) { label.classList.add("active-folder"); } ul.appendChild(li); }); return ul; } _renderBreadcrumbs() { const parts = this.currentPath.split("/").filter(Boolean); let path = "/"; const spanEls = []; spanEls.push(`<span data-path="/"><i class="bi bi-house-door-fill"></i> ${this.uiText.home}</span>`); for (let i = 0; i < parts.length; i++) { path += parts[i] + "/"; spanEls.push(`<span class="sep">/</span><span data-path="${path}">${parts[i]}</span>`); } this.breadcrumbTrail.innerHTML = spanEls.join(""); [...this.breadcrumbTrail.querySelectorAll("span[data-path]")].forEach((span) => { span.onclick = () => { this.currentPath = span.dataset.path; this.selectedFile = null; this._renderThumbnails(); this._renderPreview(); this._renderBreadcrumbs(); if (!this.container) return; this.container.querySelectorAll(".active-folder").forEach((el) => { el.classList.remove("active-folder"); }); const _label = this.folderCol.querySelector(`.folder-label[data-path="${this.currentPath}"]`); if (_label) _label.classList.add("active-folder"); }; }); } _renderThumbnails(searchValue = "") { const files = this._getFiles(this.currentPath); this.thumbGrid.innerHTML = ""; if (files.length === 0) { const emptyMsg = document.createElement("div"); emptyMsg.className = "empty-preview"; emptyMsg.innerHTML = ` <i class="bi bi-folder-x"></i> <div data-uiText="noFileInFolder">${this.uiText.noFileInFolder}</div> `; this.thumbGrid.appendChild(emptyMsg); return; } const term = this.searchTerm.toLowerCase(); const filtered = !term ? files : files.filter((f) => f.toLowerCase().includes(term)); if (!filtered.length && term) { const empty = document.createElement("div"); empty.className = "empty-preview"; empty.innerHTML = ` <i class="bi bi-folder-x empty-icon"></i> <div class="empty-text" data-uiText="noFileFound">${this.uiText.noFileFound}</div> `; this.thumbGrid.appendChild(empty); return; } filtered.forEach((file, idx) => { const div = document.createElement("div"); div.className = "thumb"; if (this.selectedFile === file) div.classList.add("selected"); // Placeholder shimmer element const skeleton = document.createElement("div"); skeleton.className = "thumbnail-skeleton"; div.appendChild(skeleton); if (this._isImage(file)) { const img = document.createElement("img"); const src = `${this.cdnUrl}/${this.currentPath}/${file}` .replace(/\/\//g, "/") .replace(/\/\//g, "/") .replace(/\/\//g, "/"); img.src = src; img.style.opacity = "0"; img.onload = () => { skeleton.remove(); img.style.opacity = "1"; }; div.append(img); } else { const icon = document.createElement("i"); icon.className = `bi ${this._getFileTypeIcon(file)} file-icon`; skeleton.remove(); div.append(icon); } const actions = document.createElement("div"); actions.className = "actions"; if (this._isImage(file)) { const eye = document.createElement("i"); eye.className = "bi bi-eye"; eye.onclick = (e) => { e.stopPropagation(); this.selectedFile = file; const imgIndex = files .filter((f) => this._isImage(f)) .indexOf(file); console.log('ast img', imgIndex, file, files, filtered); this._showImageModal(imgIndex); this._renderPreview(); }; actions.append(eye); } const del = document.createElement("i"); del.className = "bi bi-trash"; del.onclick = async (e) => { e.stopPropagation(); // const confirmed = confirm(`Delete "${file}"?`); const confirmed = await this._customConfirm(`${this.uiText.deleteConfirmationText} "${file}"`, true); if (!confirmed) return; this._toggleLoader(true, `${this.uiText.loadingDelete} : ${file}`); const __path = `${(this.basePath + "/" + this.currentPath) .replace(/\/\//g, "/") .replace(/\/\//g, "/")}${file}`.replace(/\/\//g, "/"); const res = await fetch(`${this.API_URL}/media/delete?path=${encodeURIComponent(__path)}`, { method: "DELETE", }); if (res.ok) { // this._getFiles(this.currentPath).splice(idx, 1); this._deleteFile(this.currentPath, file); if (this.selectedFile === file) this.selectedFile = null; this._renderThumbnails(); this._renderPreview(); this._toggleLoader(false); this._notify(this.uiText.deleteSuccess ?? "", "success"); } else { this._notify(this.uiText.deleteFailed ?? "", "error"); this._toggleLoader(false); } }; actions.append(del); div.append(actions); div.onclick = () => { this.selectedFile = file; this._renderThumbnails(); this._renderPreview(); }; const nameLabel = document.createElement("div"); nameLabel.className = "thumb-filename"; nameLabel.textContent = file; div.appendChild(nameLabel); this.thumbGrid.appendChild(div); }); } async _fetchFolderTree() { const res = await fetch(`${this.API_URL}/media/all_tree?base=${encodeURIComponent(this.basePath)}`); if (!res.ok) return; const data = await res.json(); this.folderTree = data; this._renderFolders(); } async _fetchFolderContent(path) { const res = await fetch(`${this.API_URL}/media/tree?path=${encodeURIComponent(path)}`); if (!res.ok) return null; return await res.json(); } joinUrlParts(...parts) { return parts .map((part, index) => { if (index === 0) return part.replace(/\/+$/, ""); // keep protocol slashes like https:// return part.replace(/^\/+|\/+$/g, ""); // strip all leading/trailing slashes from the rest }) .filter(Boolean) .join("/"); } _renderPreview() { this.previewCol.innerHTML = ""; if (!this.selectedFile) { const empty = document.createElement("div"); empty.className = "preview-empty"; empty.innerHTML = ` <i class="bi bi-file-earmark-x"></i> <div data-uiText="noFileSelected">${this.uiText.noFileSelected}</div> `; this.previewCol.appendChild(empty); return; } // Title const title = document.createElement("div"); title.className = "preview-title"; title.innerHTML = `<i class="bi bi-file-earmark-check"></i> ${this.selectedFile}`; this.previewCol.appendChild(title); const showCopySuccess = (container, message = this.uiText.copied) => { if (container.classList.contains("copied")) { return; } container.classList.add("copied"); const msg = document.createElement("div"); msg.className = "copy-msg show"; msg.innerHTML = `<i class="bi bi-check-circle-fill"></i> ${message}`; container.appendChild(msg); setTimeout(() => { msg.classList.remove("show"); container.classList.remove("copied"); setTimeout(() => msg.remove(), 300); }, 2000); }; const fileURL = this.joinUrlParts(this.cdnUrl, this.currentPath, this.selectedFile); console.log("here", this.cdnUrl, fileURL); let imgSnippet; let copyImgBtn; if (this._isImage(this.selectedFile)) { const imgTag = `<img src="${fileURL}" />`; // Create <img> code block imgSnippet = document.createElement("div"); imgSnippet.className = "code-snippet"; imgSnippet.innerHTML = ` <code>${imgTag.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</code> <i class="bi bi-clipboard copy-icon" title="Copy"></i> `; imgSnippet.querySelector(".copy-icon").onclick = () => { this._copyToClipboard(imgTag); showCopySuccess(imgSnippet, this.uiText.imgCopied); }; // Img tag copy btn copyImgBtn = document.createElement("button"); copyImgBtn.className = "btn-block"; copyImgBtn.innerHTML = `<i class="bi bi-code-slash"></i> ${this.uiText.imgCopyBtnText}`; copyImgBtn.onclick = () => { this._copyToClipboard(imgTag); showCopySuccess(imgSnippet, this.uiText.imgCopied); }; } // Create URL code block const urlSnippet = document.createElement("div"); urlSnippet.className = "code-snippet"; urlSnippet.innerHTML = ` <code>${fileURL}</code> <i class="bi bi-clipboard copy-icon" title="Copy"></i> `; const copy_ico = urlSnippet.querySelector(".copy-icon"); if (copy_ico) { copy_ico.onclick = () => { this._copyToClipboard(fileURL); showCopySuccess(urlSnippet, this.uiText.urlCopied); }; } // Buttons const copyUrlBtn = document.createElement("button"); copyUrlBtn.className = "btn-block"; copyUrlBtn.innerHTML = `<i class="bi bi-link"></i> ${this.uiText.urlCopyBtnText}`; copyUrlBtn.onclick = () => { this._copyToClipboard(fileURL); showCopySuccess(urlSnippet, this.uiText.urlCopied); }; const downloadBtn = document.createElement("button"); downloadBtn.className = "btn-block"; downloadBtn.innerHTML = `<i class="bi bi-download"></i> ${this.uiText.download}`; downloadBtn.onclick = () => { const a = document.createElement("a"); a.href = fileURL; a.target = "_blank"; a.download = this.selectedFile + ".jpg"; a.click(); }; if (this._isImage(this.selectedFile)) { this.previewCol.appendChild(imgSnippet); this.previewCol.appendChild(copyImgBtn); } this.previewCol.append(urlSnippet, copyUrlBtn, downloadBtn); } _copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(() => { }); } else { // fallback for insecure context const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.focus(); textarea.select(); try { document.execCommand("copy"); } catch (err) { console.error("Fallback copy failed:", err); } document.body.removeChild(textarea); } } _renderImageViewerModal() { const modal = document.createElement("div"); modal.id = "image-viewer-modal"; modal.className = "image-modal hidden"; modal.innerHTML = ` <div class="image-modal-content"> <span class="image-modal-close">&times;</span> <div class="image-modal-nav"> <i class="bi bi-caret-left-fill nav-left"></i> <div class="lightbox-skeleton hidden" id="image-modal-skeleton"></div> <img src="" alt="Full preview" class="lightbox-image hidden" id="image-viewer-img" /> <i class="bi bi-caret-right-fill nav-right"></i> </div> <div class="image-modal-thumbs" id="image-modal-thumbs"></div> </div> `; document.body.appendChild(modal); this.imageModal = modal; this.imageModalImg = modal.querySelector("#image-viewer-img") || undefined; this.imageModalThumbs = modal.querySelector("#image-modal-thumbs") || undefined; this.imageModalSkeleton = modal.querySelector("#image-modal-skeleton") || undefined; if (!modal) return; const imgClose = modal.querySelector(".image-modal-close"); if (imgClose) imgClose.onclick = () => this._closeImageModal(); modal.onclick = (e) => { if (e.target === modal) this._closeImageModal(); }; const navL = modal.querySelector(".nav-left"); const navR = modal.querySelector(".nav-right"); if (navL) navL.onclick = () => this._navigateImage(-1); if (navR) navR.onclick = () => this._navigateImage(1); } _showImageModal(index) { const term = this.searchTerm.toLowerCase(); const files = this._getFiles(this.currentPath); const filtered = !term ? files : files.filter((f) => f.toLowerCase().includes(term)); this.modalFiles = filtered.filter((f) => this._isImage(f)) || []; this.modalIndex = index; this._updateImageModal(); if (!this.imageModal) return; this.imageModal.classList.remove("hidden"); this.imageModal.classList.add("show"); } _closeImageModal() { if (!this.imageModal) return; this.imageModal.classList.remove("show"); setTimeout(() => { if (!this.imageModal) return; this.imageModal.classList.add("hidden"); }, 300); } _navigateImage(direction) { if (!this.modalFiles) return; this.modalIndex = (this.modalIndex + direction + this.modalFiles.length) % this.modalFiles.length; this._updateImageModal(); } _updateImageModal() { if (!this.modalFiles || this.modalIndex == undefined || !this.imageModalImg || !this.imageModalThumbs || !this.imageModalSkeleton) return; const file = this.modalFiles[this.modalIndex]; const fileName = typeof file === "string" ? file : file.name; this.selectedFile = fileName; this._renderThumbnails(); this._renderPreview(); const fileUrl = typeof file === "string" ? `${this.cdnUrl}${this.currentPath}${file}` : file.preview; // Show skeleton while loading image this.imageModalImg.classList.add("hidden"); // hide actual image this.imageModalSkeleton.classList.remove("hidden"); // show skeleton this.imageModalImg.onload = () => { if (!this.imageModalImg || !this.imageModalSkeleton) return; this.imageModalSkeleton.classList.add("hidden"); // hide skeleton this.imageModalImg.classList.remove("hidden"); // show image }; this.imageModalImg.src = fileUrl; // Render bottom thumbnails this.imageModalThumbs.innerHTML = ""; this.modalFiles.forEach((f, idx) => { if (!this.imageModalThumbs) return; const fname = typeof f === "string" ? f : f.name; const url = typeof f === "string" ? `${this.cdnUrl}${this.currentPath}${fname}` : f.preview; const thumb = document.createElement("img"); thumb.src = url; thumb.className = "modal-thumb"; if (idx === this.modalIndex) thumb.classList.add("active"); thumb.onclick = () => { this.modalIndex = idx; this._updateImageModal(); }; this.imageModalThumbs.appendChild(thumb); }); } _isImage(file) { return /\.(jpe?g|png|ico|gif|bmp|webp|svg)$/i.test(file); } _getFileTypeIcon(filename) { const pop = filename.split(".").pop(); if (!pop) return "bi-file-earmark"; const ext = pop.toLowerCase(); if (["pdf"].includes(ext)) return "bi-file-earmark-pdf"; if (["doc", "docx"].includes(ext)) return "bi-file-earmark-word"; if (["xls", "xlsx", "csv"].includes(ext)) return "bi-file-earmark-excel"; if (["zip", "rar", "tar", "gz", "7z"].includes(ext)) return "bi-file-earmark-zip"; if (["exe", "apk", "msi", "dmg", "app"].includes(ext)) return "bi-gear"; if (["mp4", "avi", "mov", "wmv", "mkv", "webm", "flv"].includes(ext)) return "bi-file-earmark-play"; if (this._isImage(filename)) return "bi-file-image"; return "bi-file-earmark"; } _notify(message, type = "info") { const iconMap = { success: "bi-check-circle-fill", error: "bi-x-circle-fill", info: "bi-info-circle-fill", }; const note = document.createElement("div"); note.className = `media-toast ${type}`; note.innerHTML = ` <i class="bi ${iconMap[type] || "bi-info-circle-fill"} toast-icon"></i> <span class="toast-message">${message}</span> `; document.body.appendChild(note); setTimeout(() => note.classList.add("show"), 10); setTimeout(() => note.classList.remove("show"), 3000); setTimeout(() => note.remove(), 3500); } _customConfirm(message, deleteConfirmation = false) { return new Promise((resolve) => { const modal = document.createElement("div"); modal.className = "custom-modal-backdrop"; modal.innerHTML = ` <div class="custom-modal"> <h3>${message}</h3> <div class="custom-modal-actions"> <button class="btn-confirm ${deleteConfirmation ? "deleteBtn" : ""}" data-uiText="confirmBtnText">${deleteConfirmation ? this.uiText.delete : this.uiText.confirmBtnText}</button> <button class="btn-cancel" data-uiText="cancelBtnText">${this.uiText.cancelBtnText}</button> </div> </div> `; document.body.appendChild(modal); const confirmBtn = modal.querySelector(".btn-confirm"); if (confirmBtn) confirmBtn.onclick = () => { modal.remove(); resolve(true); }; const cancelBtn = modal.querySelector(".btn-cancel"); if (cancelBtn) cancelBtn.onclick = () => { modal.remove(); resolve(false); }; }); } _customPrompt(message) { return new Promise((resolve) => { const modal = document.createElement("div"); modal.className = "custom-modal-backdrop"; modal.innerHTML = ` <div class="custom-modal"> <h3>${message}</h3> <div class="custom-input-container"> <i class="bi bi-folder-plus"></i> <input type="text" class="custom-input" /> </div> <div class="custom-modal-actions"> <button class="btn-confirm">${this.uiText.promptBtnText}</button> <button class="btn-cancel">${this.uiText.cancelPropmtBtnText}</button> </div> </div> `; document.body.appendChild(modal); const input = modal.querySelector("input"); if (!input) return; const confirmBtn = modal.querySelector(".btn-confirm"); if (confirmBtn) confirmBtn.onclick = () => { const val = input.value.trim(); modal.remove(); resolve(val || null); }; const cancelBtn = modal.querySelector(".btn-cancel"); if (cancelBtn) cancelBtn.onclick = () => { modal.remove(); resolve(null); }; input.focus(); }); } }