import { GxDialog } from "./dialog.js" import { GxNotice } from "./notice.js" import { requestJson } from "./startup.js" import { decodeLayoutBackup, MAX_LAYOUT_BACKUP_BYTES } from "./layout-backup.js" export const MAX_TOGGLE_BACKUP_BYTES = 256 * 1024 export const ToggleBackup = { name: "ToggleBackup", components: { GxDialog, GxNotice }, props: { unauthorized: { type: Function, required: true } }, data: () => ({ busy: false, draft: null, legacy: false, error: "", notice: "" }), methods: { async request(path, payload) { try { return await requestJson(path, { timeout: 30000, request: payload === undefined ? {} : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), } }) } catch (error) { if (error.status === 401) this.unauthorized(); throw error } }, async exportBackup() { if (this.busy) return this.busy = true; this.error = this.notice = "" try { const backup = await this.request("./api/settings/backup") const url = URL.createObjectURL(new Blob([JSON.stringify(backup, null, 2)], { type: "application/json" })) const link = document.createElement("a") try { link.href = url; link.download = "toggle-backup.json" document.body.append(link); link.click(); link.remove() } finally { link.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000) } this.notice = `${backup.settings.length} ${backup.settings.length === 1 ? "toggle" : "toggles"} backed up${backup.layout ? ", including layout and colors" : ""}.` } catch (error) { this.error = error.message || "Could not create toggle backup." } finally { this.busy = false } }, async chooseFile(event) { const file = event.target.files?.[0] event.target.value = "" if (!file || this.busy) return this.error = this.notice = "" try { if (file.size > MAX_TOGGLE_BACKUP_BYTES) throw new Error("Toggle backup is too large.") const text = await file.text(), value = JSON.parse(text) if (!value || typeof value !== "object") throw new Error("Choose a Galaxy toggle backup.") this.legacy = value.format === "starpilot-visual-layout" if (this.legacy) { if (file.size > MAX_LAYOUT_BACKUP_BYTES) throw new Error("Layout backup is too large.") this.draft = await decodeLayoutBackup(text) } else { if (value.format !== "galaxy-toggles" || value.version !== 1 || !Array.isArray(value.settings)) throw new Error("Choose a Galaxy toggle backup.") this.draft = value } } catch (error) { this.draft = null; this.error = error.message || "Could not read toggle backup." } }, async restoreBackup() { if (this.busy || !this.draft) return const draft = this.draft, legacy = this.legacy this.draft = null; this.busy = true; this.error = this.notice = "" try { if (legacy) { const current = await this.request("./api/ui/layout") if (current.editable !== true) throw new Error("Park the vehicle before restoring the layout.") const result = await this.request("./api/ui/layout", { revision: current.revision, document: draft }) if (!result.valid) throw new Error("Layout restore could not be verified.") this.notice = "Layout and colors restored." } else { const result = await this.request("./api/settings/restore", draft) if (!result.complete) throw new Error(result.error || "Restore could not be completed. Review Toggles.") this.notice = `${result.restored} ${result.restored === 1 ? "toggle" : "toggles"} restored${result.unchanged ? ` · ${result.unchanged} already matched` : ""}${result.skipped.length ? ` · ${result.skipped.length} unavailable settings skipped` : ""}${draft.layout ? ". Layout and colors restored" : ""}.` } } catch (error) { this.error = error.message || "Could not restore toggle backup." } finally { this.busy = false } }, }, template: `
Toggle backup

Save your editable toggles, onroad layout and colors. Restore compatible settings while parked.

Working…

{{ notice }}{{ error }}

Restore {{ legacy ? 'layout' : 'toggles' }}?

{{ legacy ? 'Replace your saved layout and colors?' : 'Apply the compatible saved toggles, layout and colors from this backup? Unavailable settings will be skipped.' }} Keep the vehicle parked.

`, }