diff options
| -rw-r--r-- | scss/sidebar.scss | 63 | ||||
| -rw-r--r-- | src/modules/sidebar/connectivity.tsx | 7 | ||||
| -rw-r--r-- | src/modules/sidebar/index.tsx | 6 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/bluetooth.tsx | 124 |
4 files changed, 198 insertions, 2 deletions
diff --git a/scss/sidebar.scss b/scss/sidebar.scss index 8f4d6d7..8611ed9 100644 --- a/scss/sidebar.scss +++ b/scss/sidebar.scss @@ -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); + } + } + } + } } diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx new file mode 100644 index 0000000..437e513 --- /dev/null +++ b/src/modules/sidebar/connectivity.tsx @@ -0,0 +1,7 @@ +import Bluetooth from "./modules/bluetooth"; + +export default () => ( + <box vertical className="pane connectivity" name="connectivity"> + <Bluetooth /> + </box> +); diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx index 49a118b..2a13f5c 100644 --- a/src/modules/sidebar/index.tsx +++ b/src/modules/sidebar/index.tsx @@ -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 diff --git a/src/modules/sidebar/modules/bluetooth.tsx b/src/modules/sidebar/modules/bluetooth.tsx new file mode 100644 index 0000000..c74a708 --- /dev/null +++ b/src/modules/sidebar/modules/bluetooth.tsx @@ -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> + ); +}; |