const POLL_MS = 2000; const UNITS = ["B", "KB", "MB", "GB", "TB"]; const STATE_LABELS = { off: "Off", connecting: "Connecting", online: "Online", backoff: "Reconnecting", halted: "Stopped", suspended: "Suspended", }; function formatBytes(value) { let size = Math.max(0, Number(value) || 0); let unit = 0; while (size >= 1024 && unit < UNITS.length - 1) { size /= 1024; unit += 1; } return `${unit === 0 ? size : size.toFixed(1)} ${UNITS[unit]}`; } function pad(value) { return String(value).padStart(2, "0"); } function formatInstant(iso) { const moment = new Date(iso); if (Number.isNaN(moment.getTime())) { return ""; } return `${pad(moment.getDate())}/${pad(moment.getMonth() + 1)}/${moment.getFullYear()} ${pad(moment.getHours())}:${pad(moment.getMinutes())}`; } export function describeState(state) { if (state.state === "suspended") { const ends = formatInstant(state.suspendedUntil); return ends ? `Suspended until ${ends}, retrying automatically` : "Suspended, retrying automatically"; } return state.detail || state.lastError || ""; } export function displayLabel(state, config) { return state.deviceLabel || config.label; } class Popup { constructor(root) { this.root = root; this.timer = null; this.view = null; this.$ = (id) => root.getElementById(id); } async send(message) { const reply = await chrome.runtime.sendMessage(message); if (!reply) { throw new Error("The background worker did not answer"); } if (reply.error) { throw new Error(reply.error); } return reply; } showError(error) { this.$("error").textContent = error ? error.message || String(error) : ""; } async run(message) { try { this.render(await this.send(message)); this.showError(null); } catch (error) { this.showError(error); } } render(view) { this.view = view; const { config, state, usage } = view; this.$("dot").className = `dot ${state.state}`; this.$("state").textContent = STATE_LABELS[state.state] || state.state; this.$("detail").textContent = describeState(state); this.$("label").textContent = displayLabel(state, config); this.$("disclosure-card").hidden = config.disclosed; const toggle = this.$("toggle"); toggle.checked = config.enabled; toggle.disabled = !config.disclosed || !config.hasSecret; this.$("toggle-label").textContent = config.enabled ? "Sharing on" : "Sharing off"; this.$("requests").textContent = usage.requests; this.$("failed").textContent = usage.failed; this.$("down").textContent = formatBytes(usage.bytesDown); this.$("up").textContent = formatBytes(usage.bytesUp); this.$("lifetime").textContent = `All time: ${usage.lifetimeRequests} requests, ${formatBytes(usage.lifetimeBytes)}`; this.renderSettings(config); this.renderConnection(config); } renderSettings(config) { if (this.root.activeElement && this.$("settings").contains(this.root.activeElement)) { return; } this.$("bandwidth").value = config.bandwidthMb; this.$("concurrency").value = config.maxConcurrent; this.$("from").value = config.activeFrom; this.$("to").value = config.activeTo; } renderConnection(config) { this.$("forget").hidden = !config.hasSecret; this.$("connect-summary").textContent = config.hasSecret ? "Connection" : "Connect to a server"; this.$("saved").textContent = config.hasSecret ? `Connected to ${config.serverUrl} with secret ${config.secretPrefix}` : ""; if (!config.hasSecret) { this.$("connect-box").open = true; } } async requestOrigin(serverUrl) { const origin = new URL(serverUrl).origin; const granted = await chrome.permissions.request({ origins: [origin + "/*"] }); if (!granted) { throw new Error("Permission to talk to that server was not granted"); } } async onConnect(event) { event.preventDefault(); const secretField = this.$("secret"); try { await this.requestOrigin(this.$("server").value); this.render( await this.send({ cmd: "connect", serverUrl: this.$("server").value, secret: secretField.value, label: this.$("device-label").value, }), ); secretField.value = ""; this.showError(null); } catch (error) { this.showError(error); } } onSettings(event) { event.preventDefault(); this.run({ cmd: "settings", values: { bandwidthMb: Number(this.$("bandwidth").value), maxConcurrent: Number(this.$("concurrency").value), activeFrom: this.$("from").value, activeTo: this.$("to").value, }, }); } onForget() { if (this.root.defaultView.confirm("Forget this device secret on this browser?")) { this.run({ cmd: "forget" }); } } start() { this.$("connect").addEventListener("submit", (event) => this.onConnect(event)); this.$("settings").addEventListener("submit", (event) => this.onSettings(event)); this.$("forget").addEventListener("click", () => this.onForget()); this.$("toggle").addEventListener("change", (event) => this.run({ cmd: "enable", enabled: event.target.checked }), ); this.run({ cmd: "status" }); this.timer = setInterval(() => this.run({ cmd: "status" }), POLL_MS); } } if (typeof document !== "undefined") { new Popup(document).start(); }