diff --git a/modules/common/ags/layouts/quicksettings.js b/modules/common/ags/layouts/quicksettings.js index 3a5bf5d..f4ca647 100644 --- a/modules/common/ags/layouts/quicksettings.js +++ b/modules/common/ags/layouts/quicksettings.js @@ -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} toggles + * @param {Array} 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 }))), + }), + ], }), }); diff --git a/modules/common/ags/misc.js b/modules/common/ags/misc.js index 26176b2..2517607 100644 --- a/modules/common/ags/misc.js +++ b/modules/common/ags/misc.js @@ -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), - ], - }); - } -}; diff --git a/modules/common/ags/misc/menu.js b/modules/common/ags/misc/menu.js new file mode 100644 index 0000000..3859925 --- /dev/null +++ b/modules/common/ags/misc/menu.js @@ -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, + }); diff --git a/modules/common/ags/misc/popup.js b/modules/common/ags/misc/popup.js index 6b9145e..c67e569 100644 --- a/modules/common/ags/misc/popup.js +++ b/modules/common/ags/misc/popup.js @@ -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 diff --git a/modules/common/ags/misc/utils.js b/modules/common/ags/misc/utils.js new file mode 100644 index 0000000..a1c9e3a --- /dev/null +++ b/modules/common/ags/misc/utils.js @@ -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", +}; diff --git a/modules/common/ags/modules/audio.js b/modules/common/ags/modules/audio.js index 565600c..49a5257 100644 --- a/modules/common/ags/modules/audio.js +++ b/modules/common/ags/modules/audio.js @@ -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} 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), + }), + ], + }), + }); diff --git a/modules/common/ags/services/quicksettings.js b/modules/common/ags/services/quicksettings.js index 66e2ba2..35d8a22 100644 --- a/modules/common/ags/services/quicksettings.js +++ b/modules/common/ags/services/quicksettings.js @@ -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);`); - }); - } - } - }, - ], - ], - }), - });