178 lines
6.0 KiB
TypeScript
178 lines
6.0 KiB
TypeScript
import Gtk from "gi://Gtk?version=3.0"
|
|
import AstalNetwork from "gi://AstalNetwork"
|
|
import { type Binding, type Variable, Variable, bind } from "astal"
|
|
|
|
const CONNECTING = [40, 50, 60, 70, 80, 90]
|
|
|
|
const wifi = AstalNetwork.get_default().wifi
|
|
|
|
function strongest(aps: AstalNetwork.AccessPoint[]) {
|
|
const bySsid = new Map<string, AstalNetwork.AccessPoint>()
|
|
for (const ap of aps) {
|
|
const ssid = ap.ssid ?? ""
|
|
const best = bySsid.get(ssid)
|
|
if (!best || ap.strength > best.strength) bySsid.set(ssid, ap)
|
|
}
|
|
return [...bySsid.values()].sort((a, b) => b.strength - a.strength)
|
|
}
|
|
|
|
export const wifiIcon: Binding<string> | null = wifi
|
|
? bind(wifi, "icon-name")
|
|
: null
|
|
|
|
function WifiRow(
|
|
ap: AstalNetwork.AccessPoint,
|
|
activeSsid: Binding<string>,
|
|
connect: (ap: AstalNetwork.AccessPoint) => void,
|
|
) {
|
|
return (
|
|
<button className="wifi-row" onClicked={() => connect(ap)}>
|
|
<box>
|
|
<icon icon={bind(ap, "icon-name")} />
|
|
<label
|
|
className="wifi-row-ssid"
|
|
label={ap.ssid ?? "(hidden network)"}
|
|
halign={Gtk.Align.START}
|
|
hexpand
|
|
/>
|
|
<icon
|
|
visible={ap.requires_password}
|
|
icon="network-wireless-encrypted-symbolic"
|
|
/>
|
|
<icon
|
|
visible={activeSsid.as((ssid) => ssid === ap.ssid)}
|
|
icon="object-select-symbolic"
|
|
/>
|
|
</box>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
export function WifiSection({ open }: { open: Variable<boolean> }) {
|
|
if (!wifi) return <box />
|
|
|
|
const expanded = Variable(false)
|
|
const pendingAP = Variable<AstalNetwork.AccessPoint | null>(null)
|
|
const failed = Variable("")
|
|
|
|
const status = Variable.derive(
|
|
[bind(wifi, "scanning"), bind(wifi, "state"), bind(wifi, "ssid"), failed],
|
|
(scanning, state, ssid, fail) => {
|
|
if (scanning) return "scanning…"
|
|
if (CONNECTING.includes(state)) return `connecting to ${ssid ?? "network"}…`
|
|
return fail
|
|
},
|
|
)
|
|
|
|
const activeSsid = bind(wifi, "active-access-point").as((ap) => ap?.ssid ?? "")
|
|
|
|
function connect(ap: AstalNetwork.AccessPoint) {
|
|
if (ap.requires_password) {
|
|
failed.set("")
|
|
pendingAP.set(ap)
|
|
} else {
|
|
ap.activate()
|
|
.then(() => failed.set(""))
|
|
.catch(() => failed.set(`failed to connect to ${ap.ssid ?? "network"}`))
|
|
}
|
|
}
|
|
|
|
const passwordEntry = (
|
|
<entry
|
|
className="wifi-password"
|
|
hexpand
|
|
visible={pendingAP().as((ap) => ap !== null)}
|
|
placeholderText={pendingAP().as(
|
|
(ap) => (ap ? `password for ${ap.ssid ?? "network"}` : ""),
|
|
)}
|
|
onActivate={(self) => {
|
|
const ap = pendingAP.get()
|
|
if (!ap || !self.text) return
|
|
ap.activate(self.text)
|
|
.then(() => {
|
|
pendingAP.set(null)
|
|
self.text = ""
|
|
})
|
|
.catch(() =>
|
|
failed.set(`failed to connect to ${ap.ssid ?? "network"}`)
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
|
|
open.subscribe((v) => {
|
|
if (v) {
|
|
wifi.scan()
|
|
} else {
|
|
expanded.set(false)
|
|
pendingAP.set(null)
|
|
failed.set("")
|
|
passwordEntry.text = ""
|
|
}
|
|
})
|
|
|
|
return (
|
|
<box className="wifi-section" vertical>
|
|
<box className="wifi-header">
|
|
<button
|
|
className="wifi-toggle"
|
|
onClicked={() => expanded.set(!expanded.get())}
|
|
>
|
|
<box spacing={6}>
|
|
<icon icon={bind(wifi, "icon-name")} />
|
|
<label label="Wi-Fi" />
|
|
<icon
|
|
icon={expanded().as((v) =>
|
|
v ? "pan-down-symbolic" : "pan-end-symbolic",
|
|
)}
|
|
/>
|
|
</box>
|
|
</button>
|
|
<button className="wifi-rescan" onClicked={() => wifi.scan()}>
|
|
<icon icon="view-refresh-symbolic" />
|
|
</button>
|
|
<switch
|
|
active={bind(wifi, "enabled")}
|
|
onStateSet={(_, state) => {
|
|
wifi.enabled = state
|
|
return true
|
|
}}
|
|
/>
|
|
</box>
|
|
<revealer
|
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
|
transitionDuration={180}
|
|
revealChild={bind(expanded)}
|
|
>
|
|
<box vertical>
|
|
<label
|
|
className="wifi-status"
|
|
label={status()}
|
|
visible={status().as((text) => text !== "")}
|
|
/>
|
|
{passwordEntry}
|
|
<label
|
|
className="wifi-empty"
|
|
label="wi-fi is off"
|
|
visible={bind(wifi, "enabled").as((enabled) => !enabled)}
|
|
/>
|
|
<scrollable
|
|
hscroll={Gtk.PolicyType.NEVER}
|
|
vscroll={Gtk.PolicyType.AUTOMATIC}
|
|
maxContentHeight={360}
|
|
propagateNaturalHeight
|
|
visible={bind(wifi, "enabled")}
|
|
>
|
|
<box vertical>
|
|
{bind(wifi, "access-points").as((aps) =>
|
|
strongest(aps).map((ap) =>
|
|
WifiRow(ap, activeSsid, connect)
|
|
),
|
|
)}
|
|
</box>
|
|
</scrollable>
|
|
</box>
|
|
</revealer>
|
|
</box>
|
|
)
|
|
} |