(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({