sidebar: audio device selector module
This commit is contained in:
parent
11c8a14a22
commit
72e349e368
3 changed files with 239 additions and 0 deletions
|
|
@ -507,6 +507,116 @@
|
|||
}
|
||||
}
|
||||
|
||||
.device-selector {
|
||||
@include lib.spacing(10, true);
|
||||
|
||||
.selector {
|
||||
@include lib.rounded(20);
|
||||
|
||||
background-color: color.change(scheme.$surface1, $alpha: 0.4);
|
||||
padding: lib.s(10) lib.s(15);
|
||||
|
||||
.icon {
|
||||
font-size: lib.s(20);
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-bottom: lib.s(8);
|
||||
margin-top: lib.s(5);
|
||||
background-color: if(scheme.$light, scheme.$overlay1, scheme.$overlay0);
|
||||
}
|
||||
|
||||
.list {
|
||||
color: scheme.$subtext0;
|
||||
|
||||
@include lib.spacing(3, true);
|
||||
}
|
||||
|
||||
.device {
|
||||
@include lib.spacing;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: scheme.$text;
|
||||
|
||||
@include lib.spacing(10);
|
||||
|
||||
.icon {
|
||||
font-size: lib.s(32);
|
||||
}
|
||||
|
||||
.sublabel {
|
||||
color: scheme.$subtext0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
@include lib.element-decel;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: scheme.$subtext1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: scheme.$text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream {
|
||||
@include lib.rounded(20);
|
||||
@include lib.element-decel;
|
||||
|
||||
background-color: color.change(scheme.$surface1, $alpha: 0.4);
|
||||
padding: lib.s(10) lib.s(15);
|
||||
|
||||
@include lib.spacing(5);
|
||||
|
||||
&.playing {
|
||||
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: lib.s(28);
|
||||
margin-right: lib.s(12);
|
||||
}
|
||||
|
||||
.sublabel {
|
||||
font-size: lib.s(14);
|
||||
color: scheme.$subtext0;
|
||||
}
|
||||
|
||||
trough {
|
||||
@include lib.rounded(10);
|
||||
|
||||
min-width: lib.s(100);
|
||||
min-height: lib.s(10);
|
||||
background-color: color.change(scheme.$error, $alpha: 0.3);
|
||||
|
||||
fill {
|
||||
@include lib.rounded(10);
|
||||
|
||||
background-color: color.change(scheme.$overlay0, $alpha: 1);
|
||||
}
|
||||
|
||||
highlight {
|
||||
@include lib.rounded(10);
|
||||
|
||||
background-color: scheme.$subtext1;
|
||||
}
|
||||
}
|
||||
|
||||
& > button {
|
||||
@include media-button;
|
||||
|
||||
font-size: lib.s(18);
|
||||
min-width: lib.s(20);
|
||||
min-height: lib.s(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.networks {
|
||||
.list {
|
||||
@include lib.spacing(10, true);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
126
src/modules/sidebar/modules/deviceselector.tsx
Normal file
126
src/modules/sidebar/modules/deviceselector.tsx
Normal file
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue