// 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 = `
`;
return;
}
const fs = `${remote}:`;
app.innerHTML = `
`;
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 = `
| ${t("browser.col.name")} |
${t("browser.col.size")} |
${t("browser.col.modified")} |
${t("browser.col.actions")} |
${parentHref ? `| ${t("browser.up")} | | | |
` : ""}
${rows}
`;
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, "'");
}