// views/configure.js — dynamic remote form builder. // // Two modes: // #/configure/new → provider picker (searchable grid) // #/configure/new/ → form for that backend // #/configure/edit/ → form pre-filled from /config/get // // Pulls backend metadata from /config/providers (cached in app state). // OAuth backends (option named "token" with IsPassword) get a banner // and disabled submit — user must run `rclone config` in a terminal. import { post } from "../rc.js"; import { getState, setState, toast } from "../state.js"; import { t } from "../i18n.js"; // --- Route entrypoints --- export async function renderConfigureNew({ provider = "" }) { if (!provider) { return renderProviderPicker(); } const providers = await ensureProviders(); const info = providers.find((p) => p.Name === provider); if (!info) { document.getElementById("app").innerHTML = `

${t("error.unknown_remote")}

${t("error.no_such_provider", provider)}

← ${t("configure.new_title")}

`; return; } return renderForm({ provider: info, mode: "create" }); } export async function renderConfigureEdit({ remote }) { if (!remote) { location.hash = "#/remotes"; return; } const app = document.getElementById("app"); app.innerHTML = `

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

`; let typeName; try { const dump = await post("config/dump"); typeName = dump && dump[remote] && dump[remote].type; } catch (e) { toast(`${t("error.couldnt_load")}: ${e.message}`, "error"); return; } if (!typeName) { app.innerHTML = `

${t("error.unknown_remote")}

${t("error.no_such_remote", remote)}

← ${t("nav.remotes")}

`; return; } const providers = await ensureProviders(); const info = providers.find((p) => p.Name === typeName); if (!info) { toast(`${t("error.no_such_provider", typeName)}`, "error"); return; } let currentValues = {}; try { currentValues = await post("config/get", { name: remote }); } catch (e) { toast(`${t("error.couldnt_load")}: ${e.message}`, "error"); } return renderForm({ provider: info, mode: "edit", remoteName: remote, currentValues, }); } // --- Provider picker --- async function renderProviderPicker() { const app = document.getElementById("app"); app.innerHTML = `

${t("configure.new_title")}

${t("configure.new_subtitle")}

${t("configure.cancel")}

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

`; const grid = document.getElementById("provider-grid"); const filter = document.getElementById("provider-filter"); let providers; try { providers = await ensureProviders(); } catch (e) { grid.innerHTML = `

${t("error.couldnt_load_backends")}

${escapeHtml(e.message)}

`; return; } const visible = providers.filter( (p) => !p.Hide && p.Name !== "all" && p.Name !== "alias", ); function paint(list) { if (list.length === 0) { grid.innerHTML = `

${t("configure.no_match")}

`; return; } grid.innerHTML = list .map( (p) => ` ${escapeHtml(p.Name)} ${escapeHtml(p.Description || "")} `, ) .join(""); } paint(visible); filter.addEventListener("input", () => { const q = filter.value.trim().toLowerCase(); if (!q) return paint(visible); paint( visible.filter((p) => (p.Name + " " + (p.Description || "")).toLowerCase().includes(q), ), ); }); filter.focus(); } // --- Dynamic form --- async function renderForm({ provider, mode, remoteName = "", currentValues = {} }) { const app = document.getElementById("app"); const isEdit = mode === "edit"; const requiresOAuth = provider.Options.some( (o) => o.Name === "token" && (o.IsPassword || o.Sensitive), ); // Partition options into required, optional-basic, optional-advanced. const required = provider.Options.filter((o) => o.Required && !o.Hide); const basic = provider.Options.filter((o) => !o.Required && !o.Advanced && !o.Hide); const advanced = provider.Options.filter((o) => o.Advanced && !o.Hide); app.innerHTML = `

${isEdit ? t("configure.edit_title", provider.Name) : t("configure.create_title", provider.Name)}

${escapeHtml(provider.Description || "")}

${t("configure.cancel")}
${requiresOAuth ? oauthBanner(provider.Name) : ""}
${required.length > 0 ? `
${t("configure.section.required")}
` : ""} ${required.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")} ${basic.length > 0 ? `
${t("configure.section.options")}
` : ""} ${basic.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")} ${advanced.length > 0 ? `
` : ""}
${t("configure.cancel")}
${isEdit ? `

${t("configure.delete_section_title")}

${t("configure.delete_section_body", escapeHtml(remoteName))}

` : ""}
`; const form = document.getElementById("remote-form"); // Wire up "Custom…" reveal on `; } const name = `opt_${escapeHtml(opt.Name)}`; const currentValue = value != null ? String(value) : ""; if (opt.Type === "bool") { const checked = value === true || value === "true" ? "checked" : ""; return ``; } if (opt.Type === "int" || opt.Type === "int64" || opt.Type === "Duration") { return ``; } if (opt.IsPassword) { return ``; } if (opt.Examples && opt.Examples.length > 0) { const opts = [``] .concat( opt.Examples.map( (ex) => ``, ), ) .join(""); return ` `; } return ``; } function collectParameters(form, options, isEdit) { const params = {}; for (const opt of options) { if (opt.Hide) continue; const baseName = `opt_${opt.Name}`; const el = form.elements[baseName]; if (!el) continue; let val; if (opt.Type === "bool") { val = el.checked ? "true" : "false"; } else if (el.dataset && el.dataset.hasCustom === "1" || el.getAttribute("data-has-custom") === "1") { // It's a