Files
hyprland/config/ags/modules/audio.ts
T
2024-08-08 21:29:06 +05:45

263 lines
8.1 KiB
TypeScript

import { Stream } from "types/service/audio";
import popupwindow from "./misc/popupwindow.ts";
import { MaterialIcon } from "icons.ts";
const audio = await Service.import("audio");
const WINDOW_NAME = "audio";
interface icons_type {
[key: string]: [number, string][];
}
const icons: icons_type = {
microphone: [
[1, "mic"],
[0, "mic_off"]
],
headset_mic: [
[1, "headset_mic"],
[0, "headset_off"]
],
speaker: [
[101, "sound_detection_loud_sound"],
[67, "volume_up"],
[34, "volume_down"],
[1, "volume_mute"],
[0, "volume_off"]
]
};
const off_icons = {
microphone: "mic_off",
headset_mic: "headset_off",
speaker: "volume_off"
};
const button_with_icon = (
icon: string,
size: string,
stream: Stream,
on_clicked: () => void,
icons: [number, string][],
off: string
) =>
Widget.Button({
child: MaterialIcon(icon, size).hook(stream, (self) => {
const vol = stream.volume * 100;
const icon = icons.find(([threshold]) => Number(threshold) <= vol)?.[1];
if (stream.is_muted) self.label = off;
else self.label = String(icon!);
self.tooltip_text = `${Math.floor(stream.volume * 100)}%`;
}),
on_clicked: on_clicked
});
const create_slider = (stream: Stream) =>
Widget.Slider({
min: 0,
max: 100,
draw_value: false,
hexpand: true,
vpack: "end",
value: stream.volume * 100,
on_change: (self) => {
stream.volume = self.value / 100;
self.tooltip_text = `${Math.floor(stream.volume * 100)}%`;
},
setup: (self) => {
self.hook(stream, () => {
self.max = stream.volume > 1 ? 150 : 100;
self.value = stream.volume * 100;
self.tooltip_text = `${Math.floor(stream.volume * 100)}%`;
});
}
});
const speaker = (stream: Stream, icons: [number, string][], off: string) =>
Widget.Box({
class_name: "device",
vertical: true,
attribute: { stream: stream },
children: [
Widget.Box({
children: [
Widget.Label({
label: stream.description,
xalign: 0,
truncate: "end",
hexpand: true,
tooltip_text: stream.name,
css: "margin-right: 4px;"
}),
Widget.Label({
label: stream.bind("volume").as((v) => `${Math.round(v * 100)}%`),
xalign: 1
})
]
}),
Widget.Box({
children: [
button_with_icon(
off,
"20px",
stream,
() => {
stream.is_muted = !stream.is_muted;
},
icons,
off
),
create_slider(stream)
]
})
]
});
const app = (stream: Stream) => {
const label = !!stream.name ? `${stream.name}: ${stream.description}` : stream.description;
return Widget.Box({
class_name: "device",
vertical: true,
attribute: { stream: stream },
children: [
Widget.Box({
children: [
Widget.Label({
label: label,
xalign: 0,
truncate: "end",
hexpand: true,
tooltip_text: label,
css: "margin-right: 4px;"
}),
Widget.Label({
label: stream.bind("volume").as((v) => `${Math.round(v * 100)}%`),
xalign: 1
})
]
}),
Widget.Box({
children: [
Widget.Button({
child: Widget.Icon({ icon: stream.icon_name || "image-missing", size: 20 }),
on_clicked: () => {
stream.is_muted = !stream.is_muted;
}
}),
create_slider(stream)
]
})
]
});
};
const tab = (icon: string, on_clicked: (any) => void, page?: string) =>
Widget.Overlay({
child: Widget.Button({
hexpand: true,
vexpand: true,
on_clicked: on_clicked
}),
pass_through: true,
attribute: { page: page || icon },
overlays: [MaterialIcon(icon, "22px")]
});
const audio_widget = () => {
const cur_page = Variable("speakers");
const tabs = Widget.Box({
class_name: "tabs",
children: [
tab("volume_up", () => cur_page.setValue("speakers"), "speakers"),
tab("apps", () => cur_page.setValue("apps"), "apps")
],
setup: (self) => {
self.hook(cur_page, () => {
self.children.forEach((widget) => {
widget.toggleClassName("active", widget.attribute.page == cur_page.value);
});
});
}
});
const speakers = Widget.Box({ vertical: true, children: [] as ReturnType<typeof speaker>[] });
const apps = Widget.Box({ vertical: true, children: [] as ReturnType<typeof app>[] });
function update() {
const speaker_ids = audio.speakers.map((s) => s.id);
const app_ids = audio.apps.map((s) => s.id);
const microphone_ids = audio.microphones.map((s) => s.id);
speakers.children
.filter(
(widget) =>
!speaker_ids.includes(widget.attribute.stream.id) &&
!microphone_ids.includes(widget.attribute.stream.id)
)
.forEach((widget) => widget.destroy());
apps.children
.filter((widget) => !app_ids.includes(widget.attribute.stream.id))
.forEach((widget) => widget.destroy());
audio.speakers.forEach((s) => {
if (!speakers.children.some((v) => v.attribute.stream.id == s.id)) {
speakers.pack_start(speaker(s, icons.speaker, off_icons.speaker), false, false, 0);
}
});
audio.microphones.forEach((s) => {
if (!speakers.children.some((v) => v.attribute.stream.id == s.id)) {
const _icons =
s.icon_name == "audio-headset-analog-usb"
? { off: off_icons.headset_mic, default: icons.headset_mic }
: { off: off_icons.microphone, default: icons.microphone };
speakers.pack_start(speaker(s, _icons.default, _icons.off), false, false, 0);
}
});
audio.apps.forEach((s) => {
if (!apps.children.some((v) => v.attribute.stream.id == s.id)) {
apps.pack_start(app(s), false, false, 0);
}
});
}
update();
["notify::speakers", "notify::apps", "notify::microphones"].forEach((event) => {
audio.connect(event, () => {
if (audio_popup.visible) update();
});
});
const stack = Widget.Stack({
children: { speakers, apps },
// @ts-expect-error
shown: cur_page.bind(),
transition: "crossfade"
});
return Widget.Scrollable({
child: Widget.Box({
class_name: "audio_box",
vpack: "start",
vertical: true,
children: [tabs, stack]
}),
hscroll: "never",
setup: (self) => {
self.hook(App, (_, windowName, visible) => {
if (windowName !== WINDOW_NAME) return;
if (visible) update();
});
}
});
};
export const audio_popup = popupwindow({
name: WINDOW_NAME,
class_name: "audio",
visible: false,
keymode: "exclusive",
child: audio_widget(),
anchor: ["top", "right", "bottom"]
});