up / public /index.html
semuthitamku's picture
Update public/index.html
7c3fa30 verified
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8" />
<title>File Manager</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body.theme-dark {
--bg:#0f172a; --panel:#111827; --panel-alt:#0b1221;
--muted:#94a3b8; --txt:#e5e7eb; --link:#60a5fa;
--pri:#22c55e; --danger:#ef4444; --warn:#f59e0b;
--border:#1f2937; --border2:#334155; --btn-bg:#1f2937;
--shadow: 0 4px 18px rgba(0,0,0,.25);
}
body.theme-light {
--bg:#f8fafc; --panel:#ffffff; --panel-alt:#f1f5f9;
--muted:#475569; --txt:#0f172a; --link:#2563eb;
--pri:#16a34a; --danger:#dc2626; --warn:#d97706;
--border:#e2e8f0; --border2:#cbd5e1; --btn-bg:#eef2f7;
--shadow: 0 8px 24px rgba(15,23,42,.08);
}
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Helvetica Neue',arial;
background:var(--bg); color:var(--txt); transition: background .15s, color .15s; }
header { padding:16px 20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--border); }
header h1 { margin:0; font-size:18px; font-weight:600; }
.bread a { color: var(--link); text-decoration:none; }
.bread span.sep { margin: 0 6px; color: var(--muted); }
main { padding:18px 20px; max-width:1100px; margin:auto; }
.bar { display:flex; gap:8px; flex-wrap: wrap; margin-bottom:12px; align-items:center; }
.bar input[type="text"] { background:var(--panel-alt); color:var(--txt);
border:1px solid var(--border); border-radius:8px; padding:8px 10px; min-width: 220px; }
.btn { background:var(--btn-bg); color:var(--txt); border:1px solid var(--border2);
padding:8px 12px; border-radius:8px; cursor:pointer; }
.btn:hover { filter:brightness(1.06); }
.btn.pri { background:#064e3b; border-color:#065f46; }
.btn.danger { background:#7f1d1d; border-color:#991b1b; }
.btn.warn { background:#7c2d12; border-color:#9a3412; }
.ghost { border:1px dashed var(--border); padding:10px; border-radius:8px; color:var(--muted); }
table { width:100%; border-collapse: collapse; background:var(--panel); border-radius:12px; overflow:hidden; }
th, td { padding:10px 12px; border-bottom:1px solid var(--border); }
th { text-align:left; color:var(--muted); font-weight:500; background:var(--panel-alt); }
tbody tr:hover { background:var(--panel-alt); }
.name { display:flex; align-items:center; gap:10px; }
.name a { color: var(--txt); text-decoration:none; }
.muted { color:var(--muted); }
.right { text-align:right; }
.actions { display:flex; gap:6px; }
.chip { font-size:12px; color:#0f172a; background:#a7f3d0; padding:2px 8px; border-radius:999px; }
footer { color: var(--muted); text-align:center; padding:12px; font-size:12px; }
.spacer { flex:1; }
.hidden { display:none; }
.note { font-size:12px; color:var(--muted); }
.checkbox { width:18px; height:18px; vertical-align: middle; }
.icon { width:18px; text-align:center; }
.toast { position:fixed; right:16px; bottom:16px; background:var(--panel); border:1px solid var(--border2);
padding:10px 12px; border-radius:8px; color:var(--txt); box-shadow:var(--shadow); }
label.inline { display:inline-flex; align-items:center; gap:6px; font-size:13px; color:var(--muted); }
</style>
</head>
<body class="theme-dark">
<header>
<h1>📁 File Manager</h1>
<div class="bread" id="breadcrumb"></div>
<span class="spacer"></span>
<button class="btn" id="themeToggle" title="Toggle theme">🌙 Dark</button>
<span class="note">TMPDIR</span>
</header>
<main>
<div class="bar">
<input type="text" id="urlInput" placeholder="https://contoh.com/file.tgz" />
<input type="text" id="filenameInput" placeholder="Nama file (opsional)" />
<button class="btn pri" id="fetchBtn">Save dari URL</button>
<label class="inline"><input type="checkbox" id="autoExtract" class="checkbox" checked /> Auto extract</label>
<label class="inline"><input type="checkbox" id="removeArchive" class="checkbox" /> Hapus arsip</label>
<input type="file" id="fileInput" multiple />
<button class="btn" id="uploadBtn">Upload</button>
<button class="btn" id="newFolderBtn">Folder Baru</button>
<select class="btn" id="archiveFormat" title="Pilih format arsip">
<option value="zip">ZIP</option>
<option value="tar">TAR</option>
<option value="tgz">TAR.GZ</option>
</select>
<button class="btn warn" id="zipBtn">Archive yang dipilih</button>
<button class="btn danger" id="deleteSelectedBtn">Hapus yang dipilih</button>
<span class="spacer"></span>
<span class="note">Klik nama file untuk download</span>
</div>
<div id="emptyState" class="ghost hidden">Folder ini kosong.</div>
<table id="table">
<thead>
<tr>
<th style="width:36px;"><input type="checkbox" id="selectAll" class="checkbox" /></th>
<th>Nama</th>
<th style="width:140px;">Ukuran</th>
<th style="width:200px;">Diubah</th>
<th style="width:220px;">Aksi</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<footer>
Storage root: os.tmpdir() • Mode private (tanpa autentikasi)
</footer>
</main>
<div id="toast" class="toast hidden"></div>
<script>
const tbody = document.getElementById("tbody");
const breadcrumb = document.getElementById("breadcrumb");
const emptyState = document.getElementById("emptyState");
const selectAll = document.getElementById("selectAll");
const urlInput = document.getElementById("urlInput");
const filenameInput = document.getElementById("filenameInput");
const fetchBtn = document.getElementById("fetchBtn");
const autoExtractEl = document.getElementById("autoExtract");
const removeArchiveEl = document.getElementById("removeArchive");
const fileInput = document.getElementById("fileInput");
const uploadBtn = document.getElementById("uploadBtn");
const newFolderBtn = document.getElementById("newFolderBtn");
const zipBtn = document.getElementById("zipBtn");
const deleteSelectedBtn = document.getElementById("deleteSelectedBtn");
const archiveFormat = document.getElementById("archiveFormat");
const themeToggle = document.getElementById("themeToggle");
const toast = document.getElementById("toast");
let currentPath = new URLSearchParams(location.search).get("path") || "";
// Theme
function applyTheme(t) {
document.body.classList.remove("theme-dark","theme-light");
if (t === "light") {
document.body.classList.add("theme-light");
themeToggle.textContent = "☀️ Light";
} else {
document.body.classList.add("theme-dark");
themeToggle.textContent = "🌙 Dark";
}
}
function initTheme() {
const saved = localStorage.getItem("theme");
if (saved) applyTheme(saved);
else {
const prefersLight = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches;
applyTheme(prefersLight ? "light" : "dark");
}
}
themeToggle.onclick = () => {
const nowDark = document.body.classList.contains("theme-dark");
const next = nowDark ? "light" : "dark";
applyTheme(next);
localStorage.setItem("theme", next);
};
initTheme();
function showToast(msg, ms = 2200) {
toast.textContent = msg;
toast.classList.remove("hidden");
setTimeout(() => toast.classList.add("hidden"), ms);
}
function fmtSize(bytes) {
if (bytes == null) return "-";
const units = ["B","KB","MB","GB","TB"];
let i=0, n=bytes;
while (n >= 1024 && i < units.length-1) { n/=1024; i++; }
return (Math.round(n*10)/10) + " " + units[i];
}
function fmtDate(ms) {
const d = new Date(ms);
return d.toLocaleString();
}
function setPath(rel) {
currentPath = rel || "";
const url = new URL(location.href);
if (currentPath) url.searchParams.set("path", currentPath);
else url.searchParams.delete("path");
history.replaceState(null, "", url);
load();
}
function stripArchiveExt(name) {
const lower = String(name).toLowerCase();
if (lower.endsWith(".tar.gz")) return name.slice(0, -7);
if (lower.endsWith(".tgz")) return name.slice(0, -4);
if (lower.endsWith(".zip")) return name.slice(0, -4);
if (lower.endsWith(".tar")) return name.slice(0, -4);
return name;
}
async function load() {
const res = await fetch(`/api/list?path=${encodeURIComponent(currentPath)}`);
const data = await res.json();
render(data);
}
function render(data) {
// Breadcrumb
breadcrumb.innerHTML = "";
data.breadcrumb.forEach((c, idx) => {
const a = document.createElement("a");
a.href = `?path=${encodeURIComponent(c.path)}`;
a.textContent = c.name;
a.onclick = (e) => { e.preventDefault(); setPath(c.path); };
breadcrumb.appendChild(a);
if (idx < data.breadcrumb.length - 1) {
const sep = document.createElement("span");
sep.textContent = "›";
sep.className = "sep";
breadcrumb.appendChild(sep);
}
});
// Rows
tbody.innerHTML = "";
if (!data.items.length) emptyState.classList.remove("hidden"); else emptyState.classList.add("hidden");
data.items.forEach(item => {
const tr = document.createElement("tr");
const tdSel = document.createElement("td");
const cb = document.createElement("input");
cb.type = "checkbox"; cb.className = "checkbox";
cb.dataset.name = item.name;
tdSel.appendChild(cb);
const tdName = document.createElement("td");
tdName.className = "name";
const icon = document.createElement("span");
icon.className = "icon";
icon.textContent = item.isDir ? "📂" : "📄";
const link = document.createElement("a");
link.href = "#";
link.textContent = item.name;
if (item.isDir) {
link.onclick = (e) => { e.preventDefault(); setPath(item.relPath); };
} else {
link.onclick = (e) => {
e.preventDefault();
window.location = `/api/download?path=${encodeURIComponent(item.relPath)}`;
};
}
tdName.append(icon, link);
if (!item.isDir && item.archFormat) {
const chip = document.createElement("span");
chip.className = "chip";
chip.textContent = item.archFormat === "zip" ? "ZIP" : (item.archFormat === "tar" ? "TAR" : "TGZ");
tdName.appendChild(chip);
}
const tdSize = document.createElement("td");
tdSize.className = "right muted";
tdSize.textContent = fmtSize(item.size);
const tdTime = document.createElement("td");
tdTime.className = "muted";
tdTime.textContent = fmtDate(item.mtime);
const tdAct = document.createElement("td");
const actions = document.createElement("div");
actions.className = "actions";
const renameBtn = document.createElement("button");
renameBtn.className = "btn";
renameBtn.textContent = "Rename";
renameBtn.onclick = async () => {
const newName = prompt("Nama baru:", item.name);
if (!newName || newName === item.name) return;
const res = await fetch("/api/rename", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: item.relPath, newName })
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal rename");
showToast("Berhasil di-rename");
load();
};
const delBtn = document.createElement("button");
delBtn.className = "btn danger";
delBtn.textContent = "Hapus";
delBtn.onclick = async () => {
if (!confirm(`Hapus ${item.name}?`)) return;
const res = await fetch(`/api/entry?path=${encodeURIComponent(item.relPath)}`, { method: "DELETE" });
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal hapus");
showToast("Berhasil dihapus");
load();
};
if (!item.isDir && item.archFormat) {
const unzipBtn = document.createElement("button");
unzipBtn.className = "btn warn";
unzipBtn.textContent = "Unarchive";
unzipBtn.onclick = async () => {
const defaultName = stripArchiveExt(item.name);
const destDefault = `${currentPath ? currentPath + "/" : ""}${defaultName}`;
const dest = prompt("Extract ke folder (opsional):", destDefault);
const body = dest ? { zipPath: item.relPath, destDir: dest } : { zipPath: item.relPath };
const res = await fetch("/api/unarchive", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal unarchive");
showToast("Berhasil di-extract");
load();
};
actions.appendChild(unzipBtn);
}
actions.append(renameBtn, delBtn);
tdAct.appendChild(actions);
tr.append(tdSel, tdName, tdSize, tdTime, tdAct);
tbody.appendChild(tr);
});
selectAll.checked = false;
}
async function archiveSelected() {
const selected = [...document.querySelectorAll('#tbody input[type="checkbox"]:checked')].map(cb => cb.dataset.name);
if (!selected.length) return showToast("Tidak ada yang dipilih");
let name = prompt("Nama file arsip (opsional, tanpa ekstensi):", "");
if (name === null) return;
const format = archiveFormat.value; // zip | tar | tgz
const res = await fetch("/api/archive", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: currentPath, entries: selected, name, format })
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal membuat arsip");
showToast("Arsip dibuat");
load();
}
async function deleteSelected() {
const selected = [...document.querySelectorAll('#tbody input[type="checkbox"]:checked')].map(cb => cb.dataset.name);
if (!selected.length) return showToast("Tidak ada yang dipilih");
if (!confirm(`Hapus ${selected.length} item?`)) return;
for (const name of selected) {
const rel = currentPath ? currentPath + "/" + name : name;
const res = await fetch(`/api/entry?path=${encodeURIComponent(rel)}`, { method: "DELETE" });
if (!res.ok) {
const j = await res.json().catch(() => ({}));
showToast(j.error || `Gagal hapus ${name}`);
return;
}
}
showToast("Berhasil dihapus");
load();
}
fetchBtn.onclick = async () => {
const url = urlInput.value.trim();
const filename = filenameInput.value.trim();
if (!url) return showToast("URL kosong");
const body = {
url,
destDir: currentPath,
autoExtract: autoExtractEl.checked,
removeArchive: removeArchiveEl.checked
};
if (filename) body.filename = filename;
const res = await fetch("/api/fetch-url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal download dari URL");
if (j.extractedTo) showToast(`Diunduh & diextract ke: ${j.extractedTo}`);
else showToast("Berhasil diunduh ke folder ini");
urlInput.value = ""; filenameInput.value = "";
load();
};
uploadBtn.onclick = async () => {
const files = fileInput.files;
if (!files || !files.length) return showToast("Pilih file terlebih dahulu");
const fd = new FormData();
for (const f of files) fd.append("files", f);
const res = await fetch(`/api/upload?path=${encodeURIComponent(currentPath)}`, {
method: "POST",
body: fd
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal upload");
showToast("Upload selesai");
fileInput.value = "";
load();
};
newFolderBtn.onclick = async () => {
const name = prompt("Nama folder baru:");
if (!name) return;
const res = await fetch("/api/folder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ parent: currentPath, name })
});
const j = await res.json();
if (!res.ok) return showToast(j.error || "Gagal membuat folder");
showToast("Folder dibuat");
load();
};
zipBtn.onclick = archiveSelected;
deleteSelectedBtn.onclick = deleteSelected;
selectAll.onchange = () => {
document.querySelectorAll('#tbody input[type="checkbox"]').forEach(cb => cb.checked = selectAll.checked);
};
load();
</script>
</body>
</html>