// views/browser.js — file/folder listing with breadcrumbs, mkdir, upload, delete, rename. import { post, uploadFile, downloadURL } from "../rc.js"; import { toast, formatBytes, formatTime } from "../state.js"; import { t } from "../i18n.js"; export async function renderBrowse({ remote, path }) { const app = document.getElementById("app"); if (!remote) { app.innerHTML = `

${t("browser.no_remote_title")}

${t("browser.pick_remote")} ${t("browser.no_remote_body")}

`; return; } const fs = `${remote}:`; app.innerHTML = `

${escapeHtml(remote)}

${escapeHtml(path || "(root)")}

${t("loading.title")}…

`; const breadcrumbsEl = document.getElementById("breadcrumbs"); renderBreadcrumbs(breadcrumbsEl, remote, path); const card = document.getElementById("browser-card"); const fileInput = document.getElementById("upload-input"); try { const res = await post("operations/list", { fs, remote: path || "", opt: { noModTime: false, noMimeType: true, showHash: false }, }); const items = (res && res.list) || []; renderTable(card, fs, path, items); } catch (e) { card.innerHTML = `

${t("error.couldnt_list")}

${escapeHtml(e.message)}

`; toast(`${t("browser.browse_failed")}: ${e.message}`, "error"); } app.querySelector('[data-action="mkdir"]').addEventListener("click", () => { openModal( t("browser.mkdir_title"), [ { name: "name", label: t("browser.mkdir_field"), type: "text", placeholder: t("browser.mkdir_placeholder") }, ], async ({ name }) => { if (!name) return; const target = path ? `${path}/${name}` : name; await post("operations/mkdir", { fs, remote: target }); toast(t("browser.mkdir_created", name), "success"); }, ).catch((e) => toast(`${t("browser.mkdir_failed")}: ${e.message}`, "error")); }); app.querySelector('[data-action="upload"]').addEventListener("click", () => { fileInput.value = ""; fileInput.click(); }); fileInput.addEventListener("change", async () => { const files = Array.from(fileInput.files || []); if (files.length === 0) return; try { await uploadFile(fs, path || "", files); toast(t("browser.uploaded", files.length), "success"); location.reload(); } catch (e) { toast(`${t("browser.upload_failed")}: ${e.message}`, "error"); } }); } function renderBreadcrumbs(el, remote, path) { const segments = (path || "").split("/").filter(Boolean); let html = `${t("nav.remotes")}/`; html += `${escapeHtml(remote)}`; let acc = ""; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; acc = acc ? `${acc}/${seg}` : seg; const isLast = i === segments.length - 1; html += `/`; if (isLast) { html += `${escapeHtml(seg)}`; } else { html += `${escapeHtml(seg)}`; } } el.innerHTML = html; } function renderTable(card, fs, path, items) { if (!items || items.length === 0) { card.innerHTML = `

${t("browser.empty_title")}

${t("browser.empty_body")}

`; return; } items.sort((a, b) => { if (a.IsDir !== b.IsDir) return a.IsDir ? -1 : 1; return a.Name.localeCompare(b.Name); }); const parentHref = parentLink(fs, path); const rows = items.map((item) => row(fs, path, item)).join(""); card.innerHTML = ` ${parentHref ? `` : ""} ${rows}
${t("browser.col.name")} ${t("browser.col.size")} ${t("browser.col.modified")} ${t("browser.col.actions")}
${t("browser.up")}
`; card.querySelectorAll("[data-delete]").forEach((btn) => { btn.addEventListener("click", () => onDelete(fs, path, btn.dataset.delete)); }); card.querySelectorAll("[data-rename]").forEach((btn) => { btn.addEventListener("click", () => onRename(fs, path, btn.dataset.rename)); }); } function row(fs, path, item) { const itemPath = path ? `${path}/${item.Name}` : item.Name; if (item.IsDir) { const href = `#/browse/${fs.replace(/:$/, "")}/${itemPath.split("/").map(encodeURIComponent).join("/")}`; return ` ${icon("dir")} ${escapeHtml(item.Name)}/ — ${escapeHtml(formatTime(item.ModTime))} `; } return ` ${icon("file")} ${escapeHtml(item.Name)} ${escapeHtml(formatBytes(item.Size))} ${escapeHtml(formatTime(item.ModTime))} `; } function icon(kind) { if (kind === "dir") { return ``; } return `📄`; } function parentLink(fs, path) { if (!path) return ""; const segments = path.split("/").filter(Boolean); segments.pop(); const parent = segments.join("/"); const remote = fs.replace(/:$/, ""); if (!parent) { return `#/browse/${encodeURIComponent(remote)}`; } return `#/browse/${encodeURIComponent(remote)}/${parent.split("/").map(encodeURIComponent).join("/")}`; } async function onDelete(fs, path, itemPath) { if (!confirm(t("browser.delete_confirm", itemPath))) return; try { await post("operations/deletefile", { fs, remote: itemPath }); toast(t("browser.deleted", itemPath), "success"); location.reload(); } catch (e) { toast(`${t("browser.delete_file_failed")}: ${e.message}`, "error"); } } async function onRename(fs, path, itemPath) { const segments = itemPath.split("/"); const oldName = segments.pop(); try { await openModal( t("browser.rename_title", oldName), [{ name: "name", label: t("browser.rename_field"), type: "text", value: oldName }], async ({ name }) => { if (!name || name === oldName) return; const dir = segments.join("/"); const dst = dir ? `${dir}/${name}` : name; await post("operations/movefile", { srcFs: fs, srcRemote: itemPath, dstFs: fs, dstRemote: dst, }); toast(t("browser.renamed", name), "success"); }, ); location.reload(); } catch (e) { toast(`${t("browser.rename_failed")}: ${e.message}`, "error"); } } // --- Modal helper --- export function openModal(title, fields, onSubmit) { return new Promise((resolve) => { const root = document.getElementById("modal-root"); const formHtml = fields .map( (f) => `
`, ) .join(""); root.innerHTML = ` `; const overlay = root.querySelector(".modal-overlay"); const form = root.querySelector("form"); const close = () => { root.innerHTML = ""; }; overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); form.querySelector("[data-cancel]").addEventListener("click", () => { close(); resolve(null); }); form.addEventListener("submit", async (e) => { e.preventDefault(); const data = {}; for (const f of fields) { data[f.name] = form.elements[f.name].value; } try { await onSubmit(data); close(); resolve(data); } catch (err) { toast(err.message, "error"); } }); const first = form.elements[fields[0].name]; if (first) first.focus(); }); } function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }