sidebar: connectivity pane

Bluetooth module
This commit is contained in:
2 * r + 2 * t 2025-03-26 16:54:08 +11:00
parent 0f8e6cf1c3
commit 8021e580a2
4 changed files with 198 additions and 2 deletions

View file

@ -283,4 +283,67 @@
}
}
}
.bluetooth {
.list {
@include lib.spacing(10, true);
}
.device {
@include lib.rounded(20);
background-color: color.change(scheme.$surface1, $alpha: 0.4);
padding: lib.s(10) lib.s(15);
@include lib.spacing(5);
&.connected {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
& > button {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.5);
&:hover,
&:focus {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 30%), $alpha: 0.5);
}
&:active {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 20%), $alpha: 0.5);
}
}
}
.icon {
font-size: lib.s(28);
margin-right: lib.s(12);
}
.sublabel {
font-size: lib.s(14);
color: scheme.$subtext0;
}
& > button {
@include lib.rounded(1000);
@include lib.element-decel;
@include font.icon;
font-size: lib.s(18);
min-width: lib.s(30);
min-height: lib.s(30);
background-color: color.change(scheme.$surface1, $alpha: 0.5);
&:hover,
&:focus {
background-color: color.change(scheme.$surface2, $alpha: 0.5);
}
&:active {
background-color: color.change(scheme.$overlay0, $alpha: 0.5);
}
}
}
}
}

View file

@ -0,0 +1,7 @@
import Bluetooth from "./modules/bluetooth";
export default () => (
<box vertical className="pane connectivity" name="connectivity">
<Bluetooth />
</box>
);

View file

@ -2,12 +2,13 @@ import type { Monitor } from "@/services/monitors";
import { bind, register, Variable } from "astal";
import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
import { sidebar } from "config";
import Connectivity from "./connectivity";
import Dashboard from "./dashboard";
import NotifPane from "./notifpane";
@register()
export default class SideBar extends Widget.Window {
readonly shown: Variable<string> = Variable("dashboard");
readonly shown: Variable<string>;
constructor({ monitor }: { monitor: Monitor }) {
super({
@ -20,7 +21,8 @@ export default class SideBar extends Widget.Window {
visible: sidebar.showOnStartup.get(),
});
const panes = [<Dashboard />, <NotifPane />];
const panes = [<Dashboard />, <Connectivity />, <NotifPane />];
this.shown = Variable(panes[0].name);
this.add(
<eventbox

View file

@ -0,0 +1,124 @@
import { bind, Variable } from "astal";
import { Astal, Gtk } from "astal/gtk3";
import AstalBluetooth from "gi://AstalBluetooth";
const sortDevices = (a: AstalBluetooth.Device, b: AstalBluetooth.Device) => {
if (a.connected || b.connected) return a.connected ? -1 : 1;
if (a.paired || b.paired) return a.paired ? -1 : 1;
return 0;
};
const BluetoothDevice = (device: AstalBluetooth.Device) => (
<box className={bind(device, "connected").as(c => `device ${c ? "connected" : ""}`)}>
<icon
className="icon"
icon={bind(device, "icon").as(i =>
Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic"
)}
/>
<box vertical hexpand>
<label truncate xalign={0} label={bind(device, "alias")} />
<label
truncate
className="sublabel"
xalign={0}
setup={self => {
const update = () => {
self.label = device.connected ? "Connected" : "Paired";
self.visible = device.connected || device.paired;
};
self.hook(device, "notify::connected", update);
self.hook(device, "notify::paired", update);
update();
}}
/>
</box>
<button
valign={Gtk.Align.CENTER}
visible={bind(device, "paired")}
cursor="pointer"
onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)}
label="delete"
/>
<button
valign={Gtk.Align.CENTER}
cursor="pointer"
onClicked={self => {
if (device.connected)
device.disconnect_device((_, res) => {
self.sensitive = true;
device.disconnect_device_finish(res);
});
else
device.connect_device((_, res) => {
self.sensitive = true;
device.connect_device_finish(res);
});
self.sensitive = false;
}}
label={bind(device, "connected").as(c => (c ? "bluetooth_disabled" : "bluetooth_searching"))}
/>
</box>
);
const List = ({ devNotify }: { devNotify: Variable<boolean> }) => (
<box vertical valign={Gtk.Align.START} className="list">
{bind(devNotify).as(() => AstalBluetooth.get_default().devices.sort(sortDevices).map(BluetoothDevice))}
</box>
);
const NoDevices = () => (
<box homogeneous name="empty">
<box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
<label className="icon" label="bluetooth_searching" />
<label label="No Bluetooth devices" />
</box>
</box>
);
export default () => {
const bluetooth = AstalBluetooth.get_default();
const devNotify = Variable(false); // Aggregator for device state changes (connected/paired)
const update = () => devNotify.set(!devNotify.get());
const connectSignals = (device: AstalBluetooth.Device) => {
device.connect("notify::connected", update);
device.connect("notify::paired", update);
};
bluetooth.get_devices().forEach(connectSignals);
bluetooth.connect("device-added", (_, device) => connectSignals(device));
bluetooth.connect("notify::devices", update);
return (
<box vertical className="bluetooth">
<box className="header-bar">
<label
label={bind(devNotify).as(() => {
const nConnected = bluetooth.get_devices().filter(d => d.connected).length;
return `${nConnected} connected device${nConnected === 1 ? "" : "s"}`;
})}
/>
<box hexpand />
<button
className={bind(bluetooth.adapter, "discovering").as(d => (d ? "enabled" : ""))}
cursor="pointer"
onClicked={() => {
if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery();
else bluetooth.adapter.stop_discovery();
}}
label="󰀂 Discovery"
/>
</box>
<stack
transitionType={Gtk.StackTransitionType.CROSSFADE}
transitionDuration={200}
shown={bind(bluetooth, "devices").as(d => (d.length > 0 ? "list" : "empty"))}
>
<NoDevices />
<scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
<List devNotify={devNotify} />
</scrollable>
</stack>
</box>
);
};