Add audio and sink selector in quicksettings

This commit is contained in:
2024-07-07 15:23:17 +07:00
parent 3ac43cb1e8
commit 3ffd734ec6
7 changed files with 275 additions and 285 deletions
+57 -64
View File
@@ -1,4 +1,5 @@
// import * as audio from "../modules/audio.js";
import Gtk from "gi://Gtk?version=3.0";
import * as audio from "../modules/audio.js";
// import * as brightness from "../modules/brightness.js";
// import * as network from "../modules/network.js";
// import * as bluetooth from "../modules/bluetooth.js";
@@ -18,7 +19,7 @@ const mprisService = await Service.import("mpris");
// [opened, (r) => (r.reveal_child = menuName === opened.value)],
// ],
// child: Box({
// className: "qs-submenu surface",
// className: "",
// vertical: true,
// children: [
// Box({
@@ -29,33 +30,24 @@ const mprisService = await Service.import("mpris");
// ],
// }),
// });
//
// const VolumeBox = () =>
// Box({
// vertical: true,
// children: [
// Box({
// className: "qs-slider",
// children: [
// Button({
// child: audio.SpeakerIndicator(),
// onClicked: () =>
// execAsync("pactl set-sink-mute @DEFAULT_SINK@ toggle"),
// }),
// audio.SpeakerSlider({ hexpand: true }),
// audio.SpeakerPercentLabel(),
// Arrow({ name: "stream-selector" }),
// ],
// }),
// Submenu({
// menuName: "stream-selector",
// icon: Icon("audio-volume-medium-symbolic"),
// title: "Audio Stream",
// contentType: audio.StreamSelector,
// }),
// ],
// });
//
/**
* @param {Array<Gtk.Widget>} toggles
* @param {Array<Gtk.Widget>} menus
*/
const Row = (toggles = [], menus = []) =>
Widget.Box({
vertical: true,
children: [
Widget.Box({
homogeneous: true,
class_name: "row horizontal",
children: toggles,
}),
...menus,
],
});
// const BrightnessBox = () =>
// Box({
// className: "qs-slider",
@@ -76,40 +68,41 @@ export const Quicksettings = () =>
child: Widget.Box({
vertical: true,
className: "bgcont qs-container",
children:
mprisService
.bind("players")
.as((x) => x.map((player) => mpris.MprisPlayer({ player }))),
// children: [
// VolumeBox(),
// BrightnessBox(),
// Widget.Box({
// children: [network.Toggle({}), bluetooth.Toggle({})],
// }),
// Submenu({
// menuName: "network",
// icon: "network-wireless-symbolic",
// title: "Network",
// contentType: network.Selection,
// }),
// Submenu({
// menuName: "bluetooth",
// icon: "bluetooth-symbolic",
// title: "Bluetooth",
// contentType: bluetooth.Devices,
// }),
// Box({
// children: [darkmode.DarkToggle(), nightmode.NightToggle()],
// }),
// Box({
// children: [audio.AppMixerToggle(), audio.MuteToggle()],
// }),
// Submenu({
// menuName: "app-mixer",
// icon: FontIcon({ icon: "" }),
// title: "App Mixer",
// contentType: audio.AppMixer,
// }),
// ],
children: [
Row([audio.Volume({ type: "speaker" })], [audio.SinkSelector({})]),
// BrightnessBox(),
// Widget.Box({
// children: [network.Toggle({}), bluetooth.Toggle({})],
// }),
// Submenu({
// menuName: "network",
// icon: "network-wireless-symbolic",
// title: "Network",
// contentType: network.Selection,
// }),
// Submenu({
// menuName: "bluetooth",
// icon: "bluetooth-symbolic",
// title: "Bluetooth",
// contentType: bluetooth.Devices,
// }),
// Box({
// children: [darkmode.DarkToggle(), nightmode.NightToggle()],
// }),
// Box({
// children: [audio.AppMixerToggle(), audio.MuteToggle()],
// }),
// Submenu({
// menuName: "app-mixer",
// icon: FontIcon({ icon: "" }),
// title: "App Mixer",
// contentType: audio.AppMixer,
// }),
Widget.Box({
children: mprisService
.bind("players")
.as((x) => x.map((player) => mpris.MprisPlayer({ player }))),
}),
],
}),
});
-77
View File
@@ -2,16 +2,6 @@ import App from 'resource:///com/github/Aylur/ags/app.js'
import { Box, Label, Revealer, EventBox, Overlay, Icon } from 'resource:///com/github/Aylur/ags/widget.js'
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
export const addElipsis = (str, max = 20, position = "end") => {
if (str.length <= max) return str;
switch (position) {
case "middle":
max -= 3;
return str.substring(0, Math.ceil(max / 2)) + "..." + str.substring(str.length - Math.floor(max / 2));
case "end":
return str.substring(0, max - 3) + "...";
}
};
export const Spinner = ({ icon = "process-working-symbolic" } = {}) =>
Icon({
@@ -125,70 +115,3 @@ export const Separator = ({ className = "", ...props } = {}) =>
...props,
className: `${className} separator accent`,
});
const PopupCloser = (windowName) =>
EventBox({
hexpand: true,
vexpand: true,
connections: [["button-press-event", () => App.toggleWindow(windowName)]],
});
const PopupRevealer = (windowName, transition, child) =>
Box({
css: "padding: 1px;",
children: [
Revealer({
transition,
child,
transitionDuration: 350,
connections: [
[
App,
(revealer, name, visible) => {
if (name === windowName) revealer.reveal_child = visible;
},
],
],
}),
],
});
export const PopupOverlay = (windowName, layout, child) => {
switch (layout) {
case "top":
return Box({
children: [
PopupCloser(windowName),
Box({
hexpand: false,
vertical: true,
children: [PopupRevealer(windowName, "slide_down", child), PopupCloser(windowName)],
}),
PopupCloser(windowName),
],
});
case "top right":
return Box({
children: [
PopupCloser(windowName),
Box({
hexpand: false,
vertical: true,
children: [PopupRevealer(windowName, "slide_down", child), PopupCloser(windowName)],
}),
],
});
case "center":
return Box({
children: [
PopupCloser(windowName),
Box({
hexpand: false,
vertical: true,
children: [PopupCloser(windowName), PopupRevealer(windowName, "crossfade", child), PopupCloser(windowName)],
}),
PopupCloser(windowName),
],
});
}
};
+83
View File
@@ -0,0 +1,83 @@
import Gtk from "gi://Gtk?version=3.0";
export const opened = Variable("");
App.connect("window-toggled", (_, name, visible) => {
if (name === "quicksettings" && !visible)
Utils.timeout(500, () => (opened.value = ""));
});
/**
* @param {{
* name: string,
* activate?: false | (() => void),
* } & import("types/widgets/button").ButtonProps} props
*/
export const Arrow = ({ name, activate, ...props }) => {
let deg = 0;
let iconOpened = false;
const icon = Widget.Icon("pan-end-symbolic").hook(opened, () => {
if (
(opened.value === name && !iconOpened) ||
(opened.value !== name && iconOpened)
) {
const step = opened.value === name ? 10 : -10;
iconOpened = !iconOpened;
for (let i = 0; i < 9; ++i) {
Utils.timeout(15 * i, () => {
deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
});
}
}
});
return Widget.Button({
child: icon,
className: "qs-icon",
onClicked: () => {
opened.value = opened.value === name ? "" : name;
if (typeof activate === "function") activate();
},
...props,
});
};
/**
* @typedef {{
* name: string,
* icon: string,
* title: string,
* content: Gtk.Widget[],
* } & import("types/widgets/revealer").RevealerProps} MenuProps
* @param {MenuProps} props
*/
export const Menu = ({ name, icon, title, content, ...props }) =>
Widget.Revealer({
transition: "slide_down",
reveal_child: opened.bind().as((v) => v === name),
child: Widget.Box({
className: "qs-submenu surface",
vertical: true,
children: [
Widget.Box({
className: "qs-sub-title accent",
children: [
Widget.Icon({
icon,
}),
Widget.Label({
className: "bold f16",
truncate: "end",
label: title,
}),
],
}),
Widget.Separator(),
Widget.Box({
vertical: true,
className: "qs-sub-content",
children: content,
}),
],
}),
...props,
});
+1 -1
View File
@@ -19,7 +19,7 @@ export const Padding = (
vexpand,
can_focus: false,
child: Widget.Box({ css }),
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
});
/**
* @param {string} name
+30
View File
@@ -0,0 +1,30 @@
import GLib from "gi://GLib?version=2.0"
/**
* @param {string | null | undefined} name
* @param {string | null | undefined} fallback
*/
export function icon(name, fallback) {
if (!name) return fallback || "";
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
const sub = substitutes[name];
if (sub && Utils.lookUpIcon(sub)) return sub;
if (Utils.lookUpIcon(name)) return name;
return fallback || "";
}
export const substitutes = {
"transmission-gtk": "transmission",
"blueberry.py": "blueberry",
Caprine: "facebook-messenger",
"com.raggesilver.BlackBox-symbolic": "terminal-symbolic",
"org.wezfurlong.wezterm-symbolic": "terminal-symbolic",
"audio-headset-bluetooth": "audio-headphones-symbolic",
"audio-card-analog-usb": "audio-speakers-symbolic",
"audio-card-analog-pci": "audio-card-symbolic",
"preferences-system": "emblem-system-symbolic",
"com.github.Aylur.ags-symbolic": "controls-symbolic",
"com.github.Aylur.ags": "controls-symbolic",
};
+104 -89
View File
@@ -1,6 +1,6 @@
// import { Separator, FontIcon, addElipsis } from "../misc.js";
// import { ArrowToggle, opened } from "../services/quicksettings.js";
//
import { icon } from "../misc/utils.js";
import { Arrow, Menu } from "../misc/menu.js";
const audio = await Service.import("audio");
const volumeIcons = /** @type {const} */ ([
@@ -11,16 +11,18 @@ const volumeIcons = /** @type {const} */ ([
[0, "muted"],
]);
/** @param {import("types/widgets/icon").IconProps} props */
export const VolumeIndicator = (props) =>
Widget.Icon({
icon: audio.speaker.bind("volume").as((vol) => {
const val = volumeIcons.find(
([threshold]) => threshold <= vol * 100,
)?.[1];
return `audio-volume-${val}-symbolic`;
}),
...props,
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/icon").IconProps} props */
export const VolumeIndicator = ({ type = "speaker", ...props }) =>
Widget.Icon(props).hook(audio, (self) => {
if (audio[type].is_muted) {
self.icon = "audio-volume-muted-symbolic";
self.tooltip_text = "Muted";
return;
}
const vol = audio[type].volume * 100;
const icon = volumeIcons.find(([threshold]) => threshold <= vol)?.[1];
self.icon = `audio-volume-${icon}-symbolic`;
self.tooltip_text = `Volume: ${Math.floor(vol)}%`;
});
/** @param {import("types/widgets/icon").IconProps} props */
@@ -45,23 +47,52 @@ export const MicrophoneIndicator = (props) =>
// return item;
// };
//
//
// export const SpeakerPercentLabel = (props) =>
// Label({
// ...props,
// connections: [
// [
// Audio,
// (label) => {
// if (!Audio.speaker) return;
//
// const perc = Math.floor(Audio.speaker.volume * 100);
// label.label = `${(" " + perc).slice(-3)}%`;
// },
// "speaker-changed",
// ],
// ],
// });
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/slider").SliderProps} props */
const VolumeSlider = ({ type = "speaker", ...props }) =>
Widget.Slider({
hexpand: true,
draw_value: false,
onChange: ({ value, dragging }) => {
if (dragging) {
audio[type].volume = value;
audio[type].is_muted = false;
}
},
value: audio[type].bind("volume"),
css: audio[type].bind("is_muted").as((x) => `opacity: ${x ? 0.7 : 1}`),
...props,
});
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/box").BoxProps} props */
export const Volume = ({ type = "speaker", ...props }) =>
Widget.Box({
className: "qs-slider",
children: [
Widget.Button({
onClicked: () => {
audio[type].is_muted = !audio[type].is_muted;
},
child: VolumeIndicator({ type }),
}),
VolumeSlider({ type }),
Widget.Label({
label: audio[type]
.bind("volume")
.as((vol) => `${Math.floor(vol * 100)}%`),
}),
Widget.Box({
vpack: "center",
child: Arrow({ name: "sink-selector" }),
}),
Widget.Box({
vpack: "center",
child: Arrow({ name: "app-mixer" }),
visible: audio.bind("apps").as((a) => a.length > 0),
}),
],
...props,
});
//
// export const SpeakerSlider = (props) => {
// const slider = Slider({
@@ -87,7 +118,7 @@ export const MicrophoneIndicator = (props) =>
// slider.max = 1.5;
// return slider;
// };
//
// export const MuteToggle = (props) =>
// Button({
// ...props,
@@ -192,61 +223,45 @@ export const MicrophoneIndicator = (props) =>
// ],
// });
// };
//
// export const StreamSelector = ({ streams = "speakers", ...props } = {}) =>
// Box({
// ...props,
// vertical: true,
// connections: [
// [
// Audio,
// (box) => {
// box.children = Audio[streams]
// .map((stream) =>
// Button({
// child: Box({
// children: [
// Icon({
// icon: iconSubstitute(stream.iconName),
// tooltipText: stream.iconName,
// }),
// Label(stream.description.split(" ").slice(0, 4).join(" ")),
// Icon({
// icon: "object-select-symbolic",
// hexpand: true,
// hpack: "end",
// connections: [
// [
// "draw",
// (icon) => {
// icon.visible = Audio.speaker === stream;
// },
// ],
// ],
// }),
// ],
// }),
// onClicked: () => {
// if (streams === "speakers") Audio.speaker = stream;
//
// if (streams === "microphones") Audio.microphone = stream;
// },
// }),
// )
// .concat([
// Separator(),
// Button({
// onClicked: () => {
// execAsync("pavucontrol").catch(print);
// App.closeWindow("quicksettings");
// },
// child: Label({
// label: "Settings",
// xalign: 0,
// }),
// }),
// ]);
// },
// ],
// ],
// });
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
export const SinkSelector = (props) =>
Menu({
name: "sink-selector",
icon: "audio-headphones-symbolic",
title: "Sink Selector",
content: [
Widget.Box({
vertical: true,
children: audio.bind("speakers").as((a) => a.map(SinkItem)),
}),
Widget.Separator(),
// SettingsButton(),
],
...props,
});
/** @param {import("types/service/audio").Stream} stream */
const SinkItem = (stream) =>
Widget.Button({
hexpand: true,
onClicked: () => (audio.speaker = stream),
child: Widget.Box({
css: "margin-top: 6px; margin-bottom: 6px;",
children: [
Widget.Icon({
icon: icon(stream.icon_name, "audio-x-generic-symbolic"),
tooltip_text: stream.icon_name || "",
}),
Widget.Label({
label: (stream.description || "").split(" ").slice(0, 4).join(" "),
}),
Widget.Icon({
icon: "object-select-symbolic",
hexpand: true,
hpack: "end",
visible: audio.speaker.bind("stream").as((s) => s === stream.stream),
}),
],
}),
});
@@ -1,15 +1,3 @@
import App from 'resource:///com/github/Aylur/ags/app.js'
import Service from 'resource:///com/github/Aylur/ags/service.js'
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import { Box, Button, Icon } from 'resource:///com/github/Aylur/ags/widget.js'
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
export const opened = Variable('');
App.connect('window-toggled', (_, name, visible) => {
if (name === 'quicksettings' && !visible)
timeout(500, () => opened.value = '');
});
export const ArrowToggle = ({ icon, label, toggle, name, expand, ...props }) => {
icon.className = `${icon.className} qs-icon`;
return Box({
@@ -27,45 +15,3 @@ export const ArrowToggle = ({ icon, label, toggle, name, expand, ...props }) =>
...props,
});
};
export const Arrow = ({ name, expand, ...props }) =>
Button({
...props,
className: "qs-icon",
onClicked: () => {
opened.value = opened.value === name ? "" : name;
if (expand) expand();
},
connections: [
[
opened,
(button) => {
button.toggleClassName("opened", opened.value === name);
},
],
],
child: Icon({
icon: "pan-end-symbolic",
properties: [
["deg", 0],
["opened", false],
],
connections: [
[
opened,
(icon) => {
if ((opened.value === name && !icon._opened) || (opened.value !== name && icon._opened)) {
const step = opened.value === name ? 10 : -10;
icon._opened = !icon._opened;
for (let i = 0; i < 9; ++i) {
timeout(5 * i, () => {
icon._deg += step;
icon.setCss(`-gtk-icon-transform: rotate(${icon._deg}deg);`);
});
}
}
},
],
],
}),
});