// SPDX-License-Identifier: AGPL-3.0-or-later /** * `` : a compact Wi-Fi switch showing the current network. * * Built for quick-settings surfaces (the mobile pebble tray, the desktop dock * popover), so it is deliberately cheap: it reads the radio state and the * associated AP only, and never starts NetworkManager's access-point * monitoring, which does a GetAll per visible network. */ import { LitElement, html, } from "beaver://shared/third_party/lit/lit-all.min.js"; import { networkManager, nmLog, NMDeviceState, } from "beaver://shared/dbus/network_manager.js"; // Signal strength (0-100) to a lucide icon. Thresholds match signalLevel() in // ui/settings/wifi.js, so the tile and the settings panel display the same icon. function strengthIcon(strength) { if (strength >= 75) { return "wifi"; } if (strength >= 50) { return "wifi-high"; } if (strength >= 25) { return "wifi-low"; } return "wifi-zero"; } export class WifiToggle extends LitElement { // Tears down the lifelong device-state subscription. #unwatch = null; #devicePromise = null; // The AP we're associated with, monitored for live strength. #ap = null; #onApStrength = () => { this.strength = this.#ap?.strength ?? 0; }; static properties = { // Class applied to the button, so each shell can style its own tile shape. tileClass: { type: String, attribute: "tile-class" }, on: { type: Boolean, state: true }, ssid: { type: String, state: true }, strength: { type: Number, state: true }, }; constructor() { super(); this.tileClass = "tile"; this.on = false; this.ssid = ""; this.strength = 0; } createRenderRoot() { return this; } connectedCallback() { super.connectedCallback(); this.hidden = !networkManager.available; if (!this.hidden) { this.#start(); } } disconnectedCallback() { super.disconnectedCallback(); this.#unwatch?.(); this.#unwatch = null; this.#releaseAp(); } // True when this page can reach NetworkManager at all. static get available() { return networkManager.available; } // Read the current state, and keep watching the device for changes. async #start() { await this.refresh(); const device = await this.#device(); nmLog("tile: device =", device?.path ?? null); if (!device || !this.isConnected || this.#unwatch) { return; } this.#unwatch = device.watchState((state) => { // Any transition can change what we display: ACTIVATED brings an SSID, // DISCONNECTED/UNAVAILABLE take it away. nmLog( "tile: state ->", state, "activated?", state === NMDeviceState.ACTIVATED, ); this.#syncSsid(state === NMDeviceState.ACTIVATED); }); } /** * Re-read the radio state and the associated network. */ async refresh() { if (!networkManager.available) { return; } try { this.on = await networkManager.wirelessEnabled(); await this.#syncSsid(this.on); } catch { // NetworkManager absent or refusing: keep the last known state. } } // Adopt the associated AP when we could be connected, drop it when we can't. // Holding the AP (rather than just its SSID) is what makes strength live: it // emits strength-changed as radio conditions move. async #syncSsid(maybeAssociated) { const ap = maybeAssociated ? await this.#activeAp() : null; if (ap !== this.#ap) { this.#releaseAp(); this.#ap = ap; ap?.addEventListener("strength-changed", this.#onApStrength); } this.ssid = ap?.ssid ?? ""; this.strength = ap?.strength ?? 0; nmLog( "tile: ssid =", JSON.stringify(this.ssid), "strength =", this.strength, "on =", this.on, ); } #releaseAp() { if (!this.#ap) { return; } this.#ap.removeEventListener("strength-changed", this.#onApStrength); this.#ap = null; } // The wireless device, resolved once and reused. #device() { if (!this.#devicePromise) { this.#devicePromise = networkManager .primaryWifiDevice() .catch(() => null); } return this.#devicePromise; } // The AP we're associated with, without starting device-wide AP monitoring. async #activeAp() { const device = await this.#device(); if (!device) { return null; } return (await device.currentAccessPoint()) ?? null; } #onToggle = async () => { const next = !this.on; // Optimistic: the radio takes a moment, and the tile should feel instant. // No read here - the standing watcher reports the association when it // happens. this.on = next; this.ssid = ""; try { await networkManager.setWirelessEnabled(next); } catch (error) { console.warn("[wifi-toggle] Failed to toggle Wi-Fi:", error); this.on = !next; } }; render() { const label = this.on ? this.ssid || "Not connected" : "Off"; const icon = !this.on ? "wifi-off" : this.ssid ? strengthIcon(this.strength) : "wifi-zero"; return html` `; } } customElements.define("wifi-toggle", WifiToggle);