📜
filemanager_old.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--primary { background:linear-gradient(135deg,#3b82f6,#9333ea); } .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-item.menu-open { z-index:100000; } .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; top:28px; right:6px; 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; z-index:99999; box-shadow:0 8px 16px rgba(0,0,0,0.4); } .fm-menu.open { display:block; } .fm-menu.menu-above { top: auto; bottom: 28px; } .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); } /* Toasts */ .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); } } /* Modal */ .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 = "/"; let modalResolve = null; // When opening File Manager document.addEventListener("click", async (e) => { const btn = e.target.closest(".chip"); if (btn && btn.textContent.includes("File Manager")) { currentPath = "/"; await loadFiles(currentPath); } }); // Toast helpers 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); } // Modal helpers 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); }); } // Load directory 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 contentType = res.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await res.text(); console.error('Non-JSON response:', text); throw new Error('Server returned invalid response. Check console.'); } const data = await res.json(); if (!data.success) { fmList.innerHTML = `<p style="color:#ef4444;">${data.message}</p>`; fmStatus.textContent = "Not connected or access denied"; 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 handlers 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"); if (!fmList) return; fmList.innerHTML = ""; if (!files || files.length === 0) { fmList.innerHTML = "<p>No files found.</p>"; return; } for (const f of files) { const div = document.createElement("div"); div.className = "fm-item"; div.innerHTML = ` <button class="fm-actions" aria-label="More">⋮</button> <div class="fm-menu"> ${!f.is_dir ? '<button class="fm-copy">📋 Copy to Clipboard</button>' : ''} ${!f.is_dir ? '<button class="fm-instafile">📝 InstaFile (Replace)</button>' : ''} <button class="fm-delete">🗑️ ${f.name === "_wastebasket" ? "Delete Permanently" : "Move to Trash"}</button> </div> <div class="fm-name">${f.is_dir ? "📁" : "📄"} ${f.name}</div> <div class="fm-meta">${f.is_dir ? "Folder" : formatBytes(f.size)} • ${f.modified}</div> `; // Navigate folders by clicking the name if (f.is_dir && f.name !== "_wastebasket") { div.querySelector(".fm-name").onclick = () => { const newPath = currentPath.endsWith("/") ? currentPath + f.name : currentPath + "/" + f.name; loadFiles(newPath); }; } else if (f.is_dir && f.name === "_wastebasket") { // You can still click to peek inside trash if desired: div.querySelector(".fm-name").onclick = () => { const newPath = currentPath.endsWith("/") ? currentPath + f.name : currentPath + "/" + f.name; loadFiles(newPath); }; } // Menu toggle with smart positioning const menu = div.querySelector(".fm-menu"); const toggle = div.querySelector(".fm-actions"); toggle.onclick = (e) => { e.stopPropagation(); // Close all other menus and remove z-index from all items document.querySelectorAll(".fm-menu.open").forEach(m => { m.classList.remove("open"); m.closest(".fm-item").classList.remove("menu-open"); }); // Add z-index to this item div.classList.add("menu-open"); // Open menu first (so we can measure it) menu.classList.add("open"); // Force a reflow to ensure menu is rendered menu.offsetHeight; // Now calculate positioning const toggleRect = toggle.getBoundingClientRect(); const menuRect = menu.getBoundingClientRect(); const viewportHeight = window.innerHeight; // Calculate if menu would overflow bottom of viewport const wouldOverflowBottom = menuRect.bottom > viewportHeight - 10; const hasSpaceAbove = toggleRect.top > menuRect.height; // Position menu above if it would overflow and there's space above if (wouldOverflowBottom && hasSpaceAbove) { menu.classList.add("menu-above"); } else { menu.classList.remove("menu-above"); } }; // Copy to Clipboard (files only) const copyBtn = div.querySelector(".fm-copy"); if (copyBtn) { copyBtn.onclick = async (e) => { e.stopPropagation(); menu.classList.remove("open"); const fullPath = `${currentPath.replace(/\/$/,"")}/${f.name}`; try { showToast("Reading file...", "success"); const res = await fetch("SFTPdownload.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: fullPath }) }); if (!res.ok) { const errorText = await res.text(); throw new Error(errorText || 'File not found'); } const content = await res.text(); // Copy to clipboard await navigator.clipboard.writeText(content); showToast(`✅ Copied ${formatBytes(content.length)} to clipboard!`, "success"); } catch (err) { showToast("Copy failed: " + err.message, "error"); } }; } // InstaFile - Replace file content (files only) const instaBtn = div.querySelector(".fm-instafile"); if (instaBtn) { instaBtn.onclick = async (e) => { e.stopPropagation(); menu.classList.remove("open"); const fullPath = `${currentPath.replace(/\/$/,"")}/${f.name}`; // Open modal to replace file content 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 = `📝 Replace: ${f.name}`; btnConfirm.textContent = "Replace File"; btnConfirm.className = "fm-btn fm-btn--danger"; bodyEl.innerHTML = ` <p style="color: #94a3b8; margin-bottom: 12px;"> Paste new content to replace <strong>${f.name}</strong> </p> <textarea id="instaReplaceContent" placeholder="Tap here and paste your content..." style="width: 100%; min-height: 250px; padding: 8px; background: #0f1725; border: 1px solid #2a3648; border-radius: 6px; color: #e6edf3; font-family: monospace; font-size: 13px; resize: vertical;" ></textarea> `; modal.setAttribute("aria-hidden", "false"); // Focus textarea setTimeout(() => { const textarea = document.getElementById("instaReplaceContent"); if (textarea) textarea.focus(); }, 100); const cleanup = () => { modal.setAttribute("aria-hidden", "true"); btnCancel.removeEventListener("click", onCancel); btnConfirm.removeEventListener("click", onConfirm); }; const onCancel = () => cleanup(); const onConfirm = async () => { const content = document.getElementById("instaReplaceContent").value; if (!content) { showToast("Please paste some content", "error"); return; } try { btnConfirm.disabled = true; btnConfirm.textContent = "Replacing..."; const res = await fetch("instafile.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: fullPath, content: content }) }); const data = await res.json(); if (data.success) { showToast(`✅ ${f.name} replaced successfully!`, "success"); loadFiles(currentPath); cleanup(); } else { throw new Error(data.message || 'Replace failed'); } } catch (err) { showToast("Error: " + err.message, "error"); btnConfirm.disabled = false; btnConfirm.textContent = "Replace File"; } }; btnCancel.addEventListener("click", onCancel); btnConfirm.addEventListener("click", onConfirm); }; } // Delete / Move to Trash div.querySelector(".fm-delete").onclick = async (e) => { e.stopPropagation(); menu.classList.remove("open"); const fullPath = `${currentPath.replace(/\/$/,"")}/${f.name}`; const isTrashFolder = (f.name === "_wastebasket"); if (isTrashFolder) { // permanent delete confirm const ok = await confirmModal({ title: "Permanently delete Trash?", body: "This will remove _wastebasket and all its contents. This cannot be undone.", confirmText: "Delete Trash" }); if (!ok) return; } try { const res = await fetch("SFTPtrash.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: fullPath }) }); const data = await res.json(); if (data.success) { showToast(isTrashFolder ? "Trash deleted" : "Moved to Trash"); loadFiles(currentPath); } else { showToast(data.message || "Operation failed", "error"); } } catch (err) { showToast("Error: " + err.message, "error"); } }; fmList.appendChild(div); } // Close any menu if clicking elsewhere document.addEventListener("click", (e) => { if (!e.target.closest(".fm-menu") && !e.target.closest(".fm-actions")) { document.querySelectorAll(".fm-menu.open").forEach(m => { m.classList.remove("open"); m.closest(".fm-item").classList.remove("menu-open"); }); } }); } 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({ path: `${currentPath}/${name}` }) }); const data = await res.json(); if (data.success) { showToast("File created"); loadFiles(currentPath); } else { showToast(data.message || "File create failed", "error"); } } catch (err) { showToast("Error: " + err.message, "error"); } } 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]; } console.log("[filemanager.js] Loaded - File browser component"); })();