// 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"; // --- 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 = `

Unknown backend

No provider named ${escapeHtml(provider)}.

← Back to provider picker

`; 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 = `

Loading remote ${escapeHtml(remote)}…

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

Remote not found

No remote named ${escapeHtml(remote)}.

← Back to remotes

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

New remote

Pick a storage backend to configure.

Cancel

Loading backends…

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

Couldn’t load backends

${escapeHtml(e.message)}

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

No backends 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 ? "Edit" : "New"} ${escapeHtml(provider.Name)} remote

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

Cancel
${requiresOAuth ? oauthBanner(provider.Name) : ""}
${required.length > 0 ? `
Required
` : ""} ${required.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")} ${basic.length > 0 ? `
Options
` : ""} ${basic.map((opt) => fieldHtml(opt, currentValues, isEdit)).join("")} ${advanced.length > 0 ? `
` : ""}
Cancel
${isEdit ? `

Delete this remote

Permanently remove ${escapeHtml(remoteName)} from rclone.conf.

` : ""}
`; 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 ``; } // Examples → select with custom override 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