semuthitamku commited on
Commit
5058376
·
verified ·
1 Parent(s): d7d2554

Create public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +409 -0
public/index.html ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>File Manager</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ body.theme-dark {
9
+ --bg:#0f172a; --panel:#111827; --panel-alt:#0b1221;
10
+ --muted:#94a3b8; --txt:#e5e7eb; --link:#60a5fa;
11
+ --pri:#22c55e; --danger:#ef4444; --warn:#f59e0b;
12
+ --border:#1f2937; --border2:#334155; --btn-bg:#1f2937;
13
+ --shadow: 0 4px 18px rgba(0,0,0,.25);
14
+ }
15
+ body.theme-light {
16
+ --bg:#f8fafc; --panel:#ffffff; --panel-alt:#f1f5f9;
17
+ --muted:#475569; --txt:#0f172a; --link:#2563eb;
18
+ --pri:#16a34a; --danger:#dc2626; --warn:#d97706;
19
+ --border:#e2e8f0; --border2:#cbd5e1; --btn-bg:#eef2f7;
20
+ --shadow: 0 8px 24px rgba(15,23,42,.08);
21
+ }
22
+ body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Helvetica Neue',arial;
23
+ background:var(--bg); color:var(--txt); transition: background .15s, color .15s; }
24
+ header { padding:16px 20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--border); }
25
+ header h1 { margin:0; font-size:18px; font-weight:600; }
26
+ .bread a { color: var(--link); text-decoration:none; }
27
+ .bread span.sep { margin: 0 6px; color: var(--muted); }
28
+ main { padding:18px 20px; max-width:1100px; margin:auto; }
29
+ .bar { display:flex; gap:8px; flex-wrap: wrap; margin-bottom:12px; }
30
+ .bar input[type="text"] { background:var(--panel-alt); color:var(--txt);
31
+ border:1px solid var(--border); border-radius:8px; padding:8px 10px; min-width: 220px; }
32
+ .btn { background:var(--btn-bg); color:var(--txt); border:1px solid var(--border2);
33
+ padding:8px 12px; border-radius:8px; cursor:pointer; }
34
+ .btn:hover { filter:brightness(1.06); }
35
+ .btn.pri { background:#064e3b; border-color:#065f46; }
36
+ .btn.danger { background:#7f1d1d; border-color:#991b1b; }
37
+ .btn.warn { background:#7c2d12; border-color:#9a3412; }
38
+ .ghost { border:1px dashed var(--border); padding:10px; border-radius:8px; color:var(--muted); }
39
+ table { width:100%; border-collapse: collapse; background:var(--panel); border-radius:12px; overflow:hidden; }
40
+ th, td { padding:10px 12px; border-bottom:1px solid var(--border); }
41
+ th { text-align:left; color:var(--muted); font-weight:500; background:var(--panel-alt); }
42
+ tbody tr:hover { background:var(--panel-alt); }
43
+ .name { display:flex; align-items:center; gap:10px; }
44
+ .name a { color: var(--txt); text-decoration:none; }
45
+ .muted { color: var(--muted); }
46
+ .right { text-align:right; }
47
+ .actions { display:flex; gap:6px; }
48
+ .chip { font-size:12px; color:#0f172a; background:#a7f3d0; padding:2px 8px; border-radius:999px; }
49
+ footer { color: var(--muted); text-align:center; padding:12px; font-size:12px; }
50
+ .flex-gap { display:flex; gap:8px; align-items:center; flex-wrap: wrap; }
51
+ .spacer { flex:1; }
52
+ .hidden { display:none; }
53
+ .note { font-size:12px; color:var(--muted); }
54
+ .checkbox { width:18px; height:18px; }
55
+ .icon { width:18px; text-align:center; }
56
+ .toast { position:fixed; right:16px; bottom:16px; background:var(--panel); border:1px solid var(--border2);
57
+ padding:10px 12px; border-radius:8px; color:var(--txt); box-shadow:var(--shadow); }
58
+ </style>
59
+ </head>
60
+ <body class="theme-dark">
61
+ <header>
62
+ <h1>📁 File Manager</h1>
63
+ <div class="bread" id="breadcrumb"></div>
64
+ <span class="spacer"></span>
65
+ <button class="btn" id="themeToggle" title="Toggle theme">🌙 Dark</button>
66
+ <span class="note">TMPDIR</span>
67
+ </header>
68
+
69
+ <main>
70
+ <div class="bar">
71
+ <input type="text" id="urlInput" placeholder="https://contoh.com/file.tgz" />
72
+ <input type="text" id="filenameInput" placeholder="Nama file (opsional)" />
73
+ <button class="btn pri" id="fetchBtn">Save dari URL</button>
74
+
75
+ <input type="file" id="fileInput" multiple />
76
+ <button class="btn" id="uploadBtn">Upload</button>
77
+
78
+ <button class="btn" id="newFolderBtn">Folder Baru</button>
79
+
80
+ <select class="btn" id="archiveFormat" title="Pilih format arsip">
81
+ <option value="zip">ZIP</option>
82
+ <option value="tar">TAR</option>
83
+ <option value="tgz">TAR.GZ</option>
84
+ </select>
85
+ <button class="btn warn" id="zipBtn">Archive yang dipilih</button>
86
+
87
+ <button class="btn danger" id="deleteSelectedBtn">Hapus yang dipilih</button>
88
+ <span class="spacer"></span>
89
+ <span class="note">Klik nama file untuk download</span>
90
+ </div>
91
+
92
+ <div id="emptyState" class="ghost hidden">Folder ini kosong.</div>
93
+
94
+ <table id="table">
95
+ <thead>
96
+ <tr>
97
+ <th style="width:36px;"><input type="checkbox" id="selectAll" class="checkbox" /></th>
98
+ <th>Nama</th>
99
+ <th style="width:140px;">Ukuran</th>
100
+ <th style="width:200px;">Diubah</th>
101
+ <th style="width:220px;">Aksi</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody id="tbody"></tbody>
105
+ </table>
106
+
107
+ <footer>
108
+ Storage root: os.tmpdir() • Mode private (tanpa autentikasi)
109
+ </footer>
110
+ </main>
111
+
112
+ <div id="toast" class="toast hidden"></div>
113
+
114
+ <script>
115
+ const tbody = document.getElementById("tbody");
116
+ const breadcrumb = document.getElementById("breadcrumb");
117
+ const emptyState = document.getElementById("emptyState");
118
+ const selectAll = document.getElementById("selectAll");
119
+ const urlInput = document.getElementById("urlInput");
120
+ const filenameInput = document.getElementById("filenameInput");
121
+ const fetchBtn = document.getElementById("fetchBtn");
122
+ const fileInput = document.getElementById("fileInput");
123
+ const uploadBtn = document.getElementById("uploadBtn");
124
+ const newFolderBtn = document.getElementById("newFolderBtn");
125
+ const zipBtn = document.getElementById("zipBtn");
126
+ const deleteSelectedBtn = document.getElementById("deleteSelectedBtn");
127
+ const archiveFormat = document.getElementById("archiveFormat");
128
+ const themeToggle = document.getElementById("themeToggle");
129
+ const toast = document.getElementById("toast");
130
+
131
+ let currentPath = new URLSearchParams(location.search).get("path") || "";
132
+
133
+ // Theme
134
+ function applyTheme(t) {
135
+ document.body.classList.remove("theme-dark","theme-light");
136
+ if (t === "light") {
137
+ document.body.classList.add("theme-light");
138
+ themeToggle.textContent = "☀️ Light";
139
+ } else {
140
+ document.body.classList.add("theme-dark");
141
+ themeToggle.textContent = "🌙 Dark";
142
+ }
143
+ }
144
+ function initTheme() {
145
+ const saved = localStorage.getItem("theme");
146
+ if (saved) applyTheme(saved);
147
+ else {
148
+ const prefersLight = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches;
149
+ applyTheme(prefersLight ? "light" : "dark");
150
+ }
151
+ }
152
+ themeToggle.onclick = () => {
153
+ const nowDark = document.body.classList.contains("theme-dark");
154
+ const next = nowDark ? "light" : "dark";
155
+ applyTheme(next);
156
+ localStorage.setItem("theme", next);
157
+ };
158
+ initTheme();
159
+
160
+ function showToast(msg, ms = 2200) {
161
+ toast.textContent = msg;
162
+ toast.classList.remove("hidden");
163
+ setTimeout(() => toast.classList.add("hidden"), ms);
164
+ }
165
+ function fmtSize(bytes) {
166
+ if (bytes == null) return "-";
167
+ const units = ["B","KB","MB","GB","TB"];
168
+ let i=0, n=bytes;
169
+ while (n >= 1024 && i < units.length-1) { n/=1024; i++; }
170
+ return (Math.round(n*10)/10) + " " + units[i];
171
+ }
172
+ function fmtDate(ms) {
173
+ const d = new Date(ms);
174
+ return d.toLocaleString();
175
+ }
176
+ function setPath(rel) {
177
+ currentPath = rel || "";
178
+ const url = new URL(location.href);
179
+ if (currentPath) url.searchParams.set("path", currentPath);
180
+ else url.searchParams.delete("path");
181
+ history.replaceState(null, "", url);
182
+ load();
183
+ }
184
+
185
+ async function load() {
186
+ const res = await fetch(`/api/list?path=${encodeURIComponent(currentPath)}`);
187
+ const data = await res.json();
188
+ render(data);
189
+ }
190
+
191
+ function render(data) {
192
+ breadcrumb.innerHTML = "";
193
+ data.breadcrumb.forEach((c, idx) => {
194
+ const a = document.createElement("a");
195
+ a.href = `?path=${encodeURIComponent(c.path)}`;
196
+ a.textContent = c.name;
197
+ a.onclick = (e) => { e.preventDefault(); setPath(c.path); };
198
+ breadcrumb.appendChild(a);
199
+ if (idx < data.breadcrumb.length - 1) {
200
+ const sep = document.createElement("span");
201
+ sep.textContent = "›";
202
+ sep.className = "sep";
203
+ breadcrumb.appendChild(sep);
204
+ }
205
+ });
206
+
207
+ tbody.innerHTML = "";
208
+ if (!data.items.length) emptyState.classList.remove("hidden"); else emptyState.classList.add("hidden");
209
+
210
+ data.items.forEach(item => {
211
+ const tr = document.createElement("tr");
212
+
213
+ const tdSel = document.createElement("td");
214
+ const cb = document.createElement("input");
215
+ cb.type = "checkbox"; cb.className = "checkbox";
216
+ cb.dataset.name = item.name;
217
+ tdSel.appendChild(cb);
218
+
219
+ const tdName = document.createElement("td");
220
+ tdName.className = "name";
221
+ const icon = document.createElement("span");
222
+ icon.className = "icon";
223
+ icon.textContent = item.isDir ? "📂" : "📄";
224
+ const link = document.createElement("a");
225
+ link.href = "#";
226
+ link.textContent = item.name;
227
+ if (item.isDir) {
228
+ link.onclick = (e) => { e.preventDefault(); setPath(item.relPath); };
229
+ } else {
230
+ link.onclick = (e) => {
231
+ e.preventDefault();
232
+ window.location = `/api/download?path=${encodeURIComponent(item.relPath)}`;
233
+ };
234
+ }
235
+ tdName.append(icon, link);
236
+
237
+ if (!item.isDir && item.archFormat) {
238
+ const chip = document.createElement("span");
239
+ chip.className = "chip";
240
+ chip.textContent = item.archFormat === "zip" ? "ZIP" : (item.archFormat === "tar" ? "TAR" : "TGZ");
241
+ tdName.appendChild(chip);
242
+ }
243
+
244
+ const tdSize = document.createElement("td");
245
+ tdSize.className = "right muted";
246
+ tdSize.textContent = fmtSize(item.size);
247
+
248
+ const tdTime = document.createElement("td");
249
+ tdTime.className = "muted";
250
+ tdTime.textContent = fmtDate(item.mtime);
251
+
252
+ const tdAct = document.createElement("td");
253
+ const actions = document.createElement("div");
254
+ actions.className = "actions";
255
+
256
+ const renameBtn = document.createElement("button");
257
+ renameBtn.className = "btn";
258
+ renameBtn.textContent = "Rename";
259
+ renameBtn.onclick = async () => {
260
+ const newName = prompt("Nama baru:", item.name);
261
+ if (!newName || newName === item.name) return;
262
+ const res = await fetch("/api/rename", {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json" },
265
+ body: JSON.stringify({ path: item.relPath, newName })
266
+ });
267
+ const j = await res.json();
268
+ if (!res.ok) return showToast(j.error || "Gagal rename");
269
+ showToast("Berhasil di-rename");
270
+ load();
271
+ };
272
+
273
+ const delBtn = document.createElement("button");
274
+ delBtn.className = "btn danger";
275
+ delBtn.textContent = "Hapus";
276
+ delBtn.onclick = async () => {
277
+ if (!confirm(`Hapus ${item.name}?`)) return;
278
+ const res = await fetch(`/api/entry?path=${encodeURIComponent(item.relPath)}`, { method: "DELETE" });
279
+ const j = await res.json();
280
+ if (!res.ok) return showToast(j.error || "Gagal hapus");
281
+ showToast("Berhasil dihapus");
282
+ load();
283
+ };
284
+
285
+ if (!item.isDir && item.archFormat) {
286
+ const unzipBtn = document.createElement("button");
287
+ unzipBtn.className = "btn warn";
288
+ unzipBtn.textContent = "Unarchive";
289
+ unzipBtn.onclick = async () => {
290
+ const defaultName = item.name.replace(/(\.tar\.gz|\.tgz|\.zip|\.tar)$/i, "");
291
+ const destDefault = `${currentPath ? currentPath + "/" : ""}${defaultName}`;
292
+ const dest = prompt("Extract ke folder (opsional):", destDefault);
293
+ const body = dest ? { zipPath: item.relPath, destDir: dest } : { zipPath: item.relPath };
294
+ const res = await fetch("/api/unarchive", {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json" },
297
+ body: JSON.stringify(body)
298
+ });
299
+ const j = await res.json();
300
+ if (!res.ok) return showToast(j.error || "Gagal unarchive");
301
+ showToast("Berhasil di-extract");
302
+ load();
303
+ };
304
+ actions.appendChild(unzipBtn);
305
+ }
306
+
307
+ actions.append(renameBtn, delBtn);
308
+ tdAct.appendChild(actions);
309
+
310
+ tr.append(tdSel, tdName, tdSize, tdTime, tdAct);
311
+ tbody.appendChild(tr);
312
+ });
313
+
314
+ selectAll.checked = false;
315
+ }
316
+
317
+ async function archiveSelected() {
318
+ const selected = [...document.querySelectorAll('#tbody input[type="checkbox"]:checked')].map(cb => cb.dataset.name);
319
+ if (!selected.length) return showToast("Tidak ada yang dipilih");
320
+ let name = prompt("Nama file arsip (opsional, tanpa ekstensi):", "");
321
+ if (name === null) return;
322
+ const format = archiveFormat.value; // zip | tar | tgz
323
+ const res = await fetch("/api/archive", {
324
+ method: "POST",
325
+ headers: { "Content-Type": "application/json" },
326
+ body: JSON.stringify({ path: currentPath, entries: selected, name, format })
327
+ });
328
+ const j = await res.json();
329
+ if (!res.ok) return showToast(j.error || "Gagal membuat arsip");
330
+ showToast("Arsip dibuat");
331
+ load();
332
+ }
333
+
334
+ async function deleteSelected() {
335
+ const selected = [...document.querySelectorAll('#tbody input[type="checkbox"]:checked')].map(cb => cb.dataset.name);
336
+ if (!selected.length) return showToast("Tidak ada yang dipilih");
337
+ if (!confirm(`Hapus ${selected.length} item?`)) return;
338
+ for (const name of selected) {
339
+ const rel = currentPath ? currentPath + "/" + name : name;
340
+ const res = await fetch(`/api/entry?path=${encodeURIComponent(rel)}`, { method: "DELETE" });
341
+ if (!res.ok) {
342
+ const j = await res.json().catch(() => ({}));
343
+ showToast(j.error || `Gagal hapus ${name}`);
344
+ return;
345
+ }
346
+ }
347
+ showToast("Berhasil dihapus");
348
+ load();
349
+ }
350
+
351
+ fetchBtn.onclick = async () => {
352
+ const url = urlInput.value.trim();
353
+ const filename = filenameInput.value.trim();
354
+ if (!url) return showToast("URL kosong");
355
+ const body = { url, destDir: currentPath };
356
+ if (filename) body.filename = filename;
357
+ const res = await fetch("/api/fetch-url", {
358
+ method: "POST",
359
+ headers: { "Content-Type": "application/json" },
360
+ body: JSON.stringify(body)
361
+ });
362
+ const j = await res.json();
363
+ if (!res.ok) return showToast(j.error || "Gagal download dari URL");
364
+ showToast("Berhasil diunduh ke folder ini");
365
+ urlInput.value = ""; filenameInput.value = "";
366
+ load();
367
+ };
368
+
369
+ uploadBtn.onclick = async () => {
370
+ const files = fileInput.files;
371
+ if (!files || !files.length) return showToast("Pilih file terlebih dahulu");
372
+ const fd = new FormData();
373
+ for (const f of files) fd.append("files", f);
374
+ const res = await fetch(`/api/upload?path=${encodeURIComponent(currentPath)}`, {
375
+ method: "POST",
376
+ body: fd
377
+ });
378
+ const j = await res.json();
379
+ if (!res.ok) return showToast(j.error || "Gagal upload");
380
+ showToast("Upload selesai");
381
+ fileInput.value = "";
382
+ load();
383
+ };
384
+
385
+ newFolderBtn.onclick = async () => {
386
+ const name = prompt("Nama folder baru:");
387
+ if (!name) return;
388
+ const res = await fetch("/api/folder", {
389
+ method: "POST",
390
+ headers: { "Content-Type": "application/json" },
391
+ body: JSON.stringify({ parent: currentPath, name })
392
+ });
393
+ const j = await res.json();
394
+ if (!res.ok) return showToast(j.error || "Gagal membuat folder");
395
+ showToast("Folder dibuat");
396
+ load();
397
+ };
398
+
399
+ zipBtn.onclick = archiveSelected;
400
+ deleteSelectedBtn.onclick = deleteSelected;
401
+
402
+ selectAll.onchange = () => {
403
+ document.querySelectorAll('#tbody input[type="checkbox"]').forEach(cb => cb.checked = selectAll.checked);
404
+ };
405
+
406
+ load();
407
+ </script>
408
+ </body>
409
+ </html>