📜
filecontextmenu.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
(function () { window.AppItems = window.AppItems || []; const section = { title: "File Manager", html: ` <div class="fm-container"> <div class="fm-toolbar"> <div class="fm-left"> <button id="fmUp" class="fm-btn">⬅️ Up</button> <button id="fmRefresh" class="fm-btn">🔄 Refresh</button> <button id="fmNewFolder" class="fm-btn">📁 New Folder</button> <button id="fmNewFile" class="fm-btn">📄 New File</button> <button id="fmUpload" class="fm-btn">⬆️ Upload</button> <input type="file" id="fmFileInput" style="display:none" /> </div> <span id="fmStatus" class="fm-status">Not connected</span> </div> <div id="fmList" class="fm-list"> <p style="color:#94a3b8;">Connect to your SFTP server to view files.</p> </div> </div> <!-- Toasts --> <div class="fm-toast-container" id="fmToastContainer" aria-live="polite"></div> <!-- Confirm Modal --> <div class="fm-modal" id="fmModal" aria-hidden="true" role="dialog" aria-modal="true"> <div class="fm-modal__backdrop"></div> <div class="fm-modal__dialog" role="document"> <div class="fm-modal__title" id="fmModalTitle">Confirm</div> <div class="fm-modal__body" id="fmModalBody">Are you sure?</div> <div class="fm-modal__actions"> <button class="fm-btn fm-btn--ghost" id="fmModalCancel">Cancel</button> <button class="fm-btn fm-btn--danger" id="fmModalConfirm">Delete</button> </div> </div> </div> <style> .fm-container { display:flex; flex-direction:column; height:100%; } .fm-toolbar { display:flex; justify-content:space-between; align-items:center; background:rgba(30,41,59,0.8); border-bottom:1px solid rgba(71,85,105,0.3); padding:0.5rem 1rem; border-radius:0.5rem 0.5rem 0 0; flex-wrap:wrap; gap:0.5rem; } .fm-left { display:flex; flex-wrap:wrap; gap:0.5rem; } .fm-btn { background:linear-gradient(135deg,#3b82f6,#9333ea); border:none; border-radius:6px; color:white; padding:0.4rem 0.75rem; cursor:pointer; font-weight:600; font-size:0.85rem; } .fm-btn:hover { opacity:0.9; } .fm-btn--ghost { background:transparent; border:1px solid rgba(71,85,105,0.6); color:#e2e8f0; } .fm-btn--danger { background:linear-gradient(135deg,#ef4444,#dc2626); } .fm-status { font-size:0.9rem; color:#94a3b8; } .fm-list { flex:1; overflow-y:auto; padding:1rem; display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:0.75rem; } .fm-item { position:relative; background:rgba(30,41,59,0.7); border:1px solid rgba(71,85,105,0.4); border-radius:8px; padding:0.75rem; display:flex; flex-direction:column; justify-content:space-between; transition:all 0.15s ease; z-index:1; } .fm-item:hover { background:rgba(51,65,85,0.9); transform:translateY(-2px); } .fm-name { font-weight:600; color:#e2e8f0; word-break:break-all; cursor:pointer; } .fm-meta { font-size:0.8rem; color:#94a3b8; margin-top:0.25rem; } .fm-actions { position:absolute; top:6px; right:6px; background:transparent; border:none; color:#9aa4b2; font-size:1rem; cursor:pointer; } .fm-menu { display:none; position:absolute; background:rgba(15,23,42,0.98); border:1px solid rgba(71,85,105,0.4); border-radius:8px; padding:4px 0; min-width:160px; box-shadow:0 8px 16px rgba(0,0,0,0.4); z-index:99999; } .fm-menu.open { display:block; } .fm-menu button { display:block; width:100%; background:none; border:none; color:#e2e8f0; text-align:left; padding:8px 12px; font-size:0.85rem; cursor:pointer; } .fm-menu button:hover { background:rgba(51,65,85,0.8); } .fm-toast-container { position: fixed; right: 16px; bottom: 16px; z-index: 99999; display: flex; flex-direction: column; gap: 8px; } .fm-toast { background: rgba(15,23,42,0.98); border:1px solid rgba(71,85,105,0.5); color:#e2e8f0; padding:10px 14px; border-radius:10px; box-shadow:0 8px 20px rgba(0,0,0,0.35); font-size: 0.9rem; opacity: 0; transform: translateY(8px); animation: fm-toast-in 200ms ease forwards; } .fm-toast--success { border-color: rgba(16,185,129,0.6); } .fm-toast--error { border-color: rgba(239,68,68,0.6); } @keyframes fm-toast-in { to { opacity:1; transform: translateY(0); } } @keyframes fm-toast-out { to { opacity:0; transform: translateY(8px); } } .fm-modal { position: fixed; inset: 0; display: none; z-index: 99998; } .fm-modal[aria-hidden="false"] { display: block; } .fm-modal__backdrop { position:absolute; inset:0; background:rgba(0,0,0,0.5); backdrop-filter: blur(2px); } .fm-modal__dialog { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:min(92vw,420px); background:rgba(15,23,42,0.98); color:#e2e8f0; border:1px solid rgba(71,85,105,0.5); border-radius:12px; box-shadow:0 20px 60px rgba(0,0,0,0.45); padding:16px; } .fm-modal__title { font-weight:800; margin-bottom:8px; } .fm-modal__body { color:#cbd5e1; margin-bottom:12px; } .fm-modal__actions { display:flex; justify-content:flex-end; gap:8px; } </style> ` }; window.AppItems.push(section); let currentPath = "/"; /*** ========== Core Utilities ========== ***/ function showToast(message, type = "success", timeout = 2200) { const cont = document.getElementById("fmToastContainer"); if (!cont) return; const t = document.createElement("div"); t.className = `fm-toast ${type === "error" ? "fm-toast--error" : "fm-toast--success"}`; t.textContent = message; cont.appendChild(t); setTimeout(() => { t.style.animation = "fm-toast-out 160ms ease forwards"; setTimeout(() => cont.removeChild(t), 170); }, timeout); } function confirmModal({ title = "Confirm", body = "Are you sure?", confirmText = "Delete" } = {}) { const modal = document.getElementById("fmModal"); const titleEl = document.getElementById("fmModalTitle"); const bodyEl = document.getElementById("fmModalBody"); const btnCancel = document.getElementById("fmModalCancel"); const btnConfirm = document.getElementById("fmModalConfirm"); titleEl.textContent = title; bodyEl.textContent = body; btnConfirm.textContent = confirmText; modal.setAttribute("aria-hidden", "false"); return new Promise((resolve) => { const cleanup = () => { modal.setAttribute("aria-hidden", "true"); btnCancel.removeEventListener("click", onCancel); btnConfirm.removeEventListener("click", onConfirm); document.removeEventListener("keydown", onEsc); }; const onCancel = () => { cleanup(); resolve(false); }; const onConfirm = () => { cleanup(); resolve(true); }; const onEsc = (e) => { if (e.key === "Escape") onCancel(); }; btnCancel.addEventListener("click", onCancel); btnConfirm.addEventListener("click", onConfirm); document.addEventListener("keydown", onEsc); }); } function formatBytes(bytes) { if (bytes < 1024) return bytes + " B"; const units = ["KB", "MB", "GB", "TB"]; let u = -1; do { bytes /= 1024; ++u; } while (bytes >= 1024 && u < units.length - 1); return bytes.toFixed(1) + " " + units[u]; } /*** ========== Embedded Context Menu System ========== ***/ window.FileContextMenu = { createContextMenu(target, actions = []) { if (!target || !actions.length) return; const menu = document.createElement("div"); menu.className = "fm-menu"; actions.forEach(({ label, icon, action }) => { const btn = document.createElement("button"); btn.innerHTML = `${icon ? icon + " " : ""}${label}`; btn.onclick = (e) => { e.stopPropagation(); FileContextMenu.closeAllMenus(); if (typeof action === "function") action(); }; menu.appendChild(btn); }); document.body.appendChild(menu); target.addEventListener("click", (e) => { e.stopPropagation(); const isOpen = menu.style.display === "block"; FileContextMenu.closeAllMenus(); if (isOpen) return; menu.style.display = "block"; const rect = target.getBoundingClientRect(); menu.style.left = `${rect.right - menu.offsetWidth}px`; menu.style.top = `${rect.bottom + 4}px`; const menuRect = menu.getBoundingClientRect(); if (menuRect.bottom > window.innerHeight) menu.style.top = `${rect.top - menu.offsetHeight - 4}px`; }); }, closeAllMenus() { document.querySelectorAll(".fm-menu").forEach((m) => (m.style.display = "none")); } }; document.addEventListener("click", (e) => { if (!e.target.closest(".fm-menu")) FileContextMenu.closeAllMenus(); }); window.addEventListener("scroll", FileContextMenu.closeAllMenus); window.addEventListener("resize", FileContextMenu.closeAllMenus); /*** ========== Load Commands ========== ***/ window.FileCommands = window.FileCommands || []; const commandFiles = ["copyfile.js", "instafile.js", "deletefile.js"]; commandFiles.forEach(f => { const s = document.createElement("script"); s.src = `commands/${f}`; s.onload = () => console.log(`[filemanager] Loaded command: ${f}`); document.head.appendChild(s); }); /*** ========== File Loading ========== ***/ document.addEventListener("click", async (e) => { const btn = e.target.closest(".chip"); if (btn && btn.textContent.includes("File Manager")) { currentPath = "/"; await loadFiles(currentPath); } }); async function loadFiles(path = "/") { const fmList = document.getElementById("fmList"); const fmStatus = document.getElementById("fmStatus"); if (!fmList) return; fmList.textContent = "Loading..."; fmStatus.textContent = `Loading ${path}...`; try { const res = await fetch("SFTPconnector.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "list", path }) }); const data = await res.json(); if (!data.success) { fmList.innerHTML = `<p style="color:#ef4444;">${data.message}</p>`; fmStatus.textContent = "Not connected"; return; } renderFiles(data.data); fmStatus.textContent = `Path: ${path} (${data.data.length} items)`; currentPath = path; } catch (err) { fmList.innerHTML = `<p style="color:#ef4444;">Error: ${err.message}</p>`; fmStatus.textContent = "Error"; } // Toolbar document.getElementById("fmRefresh").onclick = () => loadFiles(currentPath); document.getElementById("fmUp").onclick = () => goUpDirectory(); document.getElementById("fmNewFolder").onclick = () => createFolderPrompt(); document.getElementById("fmNewFile").onclick = () => createFilePrompt(); document.getElementById("fmUpload").onclick = () => document.getElementById("fmFileInput").click(); const fileInput = document.getElementById("fmFileInput"); fileInput.onchange = async () => { const file = fileInput.files[0]; if (!file) return; const formData = new FormData(); formData.append("action", "upload"); formData.append("path", currentPath); formData.append("file", file); try { const res = await fetch("SFTPupload.php", { method: "POST", body: formData }); const data = await res.json(); if (data.success) { showToast("Uploaded successfully"); loadFiles(currentPath); } else { showToast(data.message || "Upload failed", "error"); } } catch (err) { showToast("Upload error: " + err.message, "error"); } finally { fileInput.value = ""; } }; } function renderFiles(files) { const fmList = document.getElementById("fmList"); fmList.innerHTML = ""; if (!files || !files.length) { fmList.innerHTML = "<p>No files found.</p>"; return; } files.forEach(file => { const div = document.createElement("div"); div.className = "fm-item"; div.innerHTML = ` <button class="fm-actions" aria-label="More">⋮</button> <div class="fm-name">${file.is_dir ? "📁" : "📄"} ${file.name}</div> <div class="fm-meta">${file.is_dir ? "Folder" : formatBytes(file.size)} • ${file.modified}</div> `; if (file.is_dir) { div.querySelector(".fm-name").onclick = () => { const newPath = currentPath.endsWith("/") ? currentPath + file.name : currentPath + "/" + file.name; loadFiles(newPath); }; } const actions = (window.FileCommands || []) .filter(cmd => !cmd.appliesTo || cmd.appliesTo(file)) .map(cmd => ({ label: cmd.label, action: () => cmd.action({ file, currentPath, showToast, confirmModal, reload: () => loadFiles(currentPath) }) })); if (actions.length) { const trigger = div.querySelector(".fm-actions"); window.FileContextMenu.createContextMenu(trigger, actions); } fmList.appendChild(div); }); } function goUpDirectory() { if (currentPath === "/" || currentPath === "") return; const parts = currentPath.split("/").filter(Boolean); parts.pop(); const newPath = "/" + parts.join("/"); loadFiles(newPath === "/" ? "/" : newPath); } async function createFolderPrompt() { const name = prompt("Enter new folder name:"); if (!name) return; try { const res = await fetch("SFTPconnector.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "create_folder", path: `${currentPath}/${name}` }) }); const data = await res.json(); if (data.success) { showToast("Folder created"); loadFiles(currentPath); } else { showToast(data.message || "Folder create failed", "error"); } } catch (err) { showToast("Error: " + err.message, "error"); } } async function createFilePrompt() { const name = prompt("Enter new file name (e.g. newfile.txt):"); if (!name) return; try { const res = await fetch("SFTPnewfile.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({