diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-03-27 18:02:40 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-03-27 18:02:40 +1100 |
| commit | 72e349e368f6d4600512354e75551b380cbf70c7 (patch) | |
| tree | 0668c7b511457c84dc2dd3691e13004943119d27 /src | |
| parent | sidebar: change streams unavailable text (diff) | |
| download | caelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.tar.gz caelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.tar.bz2 caelestia-shell-72e349e368f6d4600512354e75551b380cbf70c7.zip | |
sidebar: audio device selector module
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/sidebar/audio.tsx | 3 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/deviceselector.tsx | 126 |
2 files changed, 129 insertions, 0 deletions
diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx index a5c1651..20a6551 100644 --- a/src/modules/sidebar/audio.tsx +++ b/src/modules/sidebar/audio.tsx @@ -1,3 +1,4 @@ +import DeviceSelector from "./modules/deviceselector"; import Media from "./modules/media"; import Streams from "./modules/streams"; @@ -6,5 +7,7 @@ export default () => ( <Media /> <box className="separator" /> <Streams /> + <box className="separator" /> + <DeviceSelector /> </box> ); diff --git a/src/modules/sidebar/modules/deviceselector.tsx b/src/modules/sidebar/modules/deviceselector.tsx new file mode 100644 index 0000000..03acc97 --- /dev/null +++ b/src/modules/sidebar/modules/deviceselector.tsx @@ -0,0 +1,126 @@ +import { bind, execAsync, Variable, type Binding } from "astal"; +import { Astal, Gtk } from "astal/gtk3"; +import AstalWp from "gi://AstalWp"; + +const Device = ({ + input, + defaultDevice, + showDropdown, + device, +}: { + input?: boolean; + defaultDevice: Binding<AstalWp.Endpoint>; + showDropdown: Variable<boolean>; + device: AstalWp.Endpoint; +}) => ( + <button + visible={defaultDevice.get().id !== device.id} + cursor="pointer" + onClicked={() => { + execAsync(`wpctl set-default ${device.id}`).catch(console.error); + showDropdown.set(false); + }} + setup={self => { + let last: { d: AstalWp.Endpoint; id: number } | null = { + d: defaultDevice.get(), + id: defaultDevice + .get() + .connect("notify::id", () => self.set_visible(defaultDevice.get().id !== device.id)), + }; + self.hook(defaultDevice, (_, d) => { + last?.d.disconnect(last.id); + self.set_visible(d.id !== device.id); + last = { + d, + id: d.connect("notify::id", () => self.set_visible(d.id !== device.id)), + }; + }); + self.connect("destroy", () => last?.d.disconnect(last.id)); + }} + > + <box className="device"> + {bind(device, "icon").as(i => + Astal.Icon.lookup_icon(i) ? ( + <icon className="icon" icon={device.icon} /> + ) : ( + <label className="icon" label={input ? "mic" : "media_output"} /> + ) + )} + <label label={bind(device, "description")} /> + </box> + </button> +); + +const DefaultDevice = ({ input, device }: { input?: boolean; device: AstalWp.Endpoint }) => ( + <box className="selected"> + <label className="icon" label={input ? "mic" : "media_output"} /> + <box vertical> + <label + truncate + xalign={0} + label={bind(device, "description").as(d => (input ? "[In] " : "[Out] ") + (d ?? "Unknown"))} + /> + <label + xalign={0} + className="sublabel" + label={bind(device, "volume").as(v => `Volume ${Math.round(v * 100)}%`)} + /> + </box> + </box> +); + +const Selector = ({ input, audio }: { input?: boolean; audio: AstalWp.Audio }) => { + const showDropdown = Variable(false); + const defaultDevice = bind(audio, input ? "defaultMicrophone" : "defaultSpeaker"); + + return ( + <box vertical className="selector"> + <revealer + transitionType={Gtk.RevealerTransitionType.SLIDE_UP} + transitionDuration={150} + revealChild={bind(showDropdown)} + > + <box vertical className="list"> + {bind(audio, input ? "microphones" : "speakers").as(ds => + ds.map(d => ( + <Device + input={input} + defaultDevice={defaultDevice} + showDropdown={showDropdown} + device={d} + /> + )) + )} + <box className="separator" /> + </box> + </revealer> + <button cursor="pointer" onClick={() => showDropdown.set(!showDropdown.get())}> + {defaultDevice.as(d => ( + <DefaultDevice input={input} device={d} /> + ))} + </button> + </box> + ); +}; + +const NoWp = () => ( + <box vexpand homogeneous> + <box vertical valign={Gtk.Align.CENTER}> + <label label="Device selector unavailable" /> + <label className="no-wp-prompt" label="WirePlumber is required for this module" /> + </box> + </box> +); + +export default () => { + const audio = AstalWp.get_default()?.get_audio(); + + if (!audio) return <NoWp />; + + return ( + <box vertical className="device-selector"> + <Selector input audio={audio} /> + <Selector audio={audio} /> + </box> + ); +}; |