diff options
Diffstat (limited to '.config/ags/widget/quicksettings')
| -rw-r--r-- | .config/ags/widget/quicksettings/QuickSettings.ts | 84 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/ToggleButton.ts | 154 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/quicksettings.scss | 177 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Bluetooth.ts | 61 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Brightness.ts | 23 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/DND.ts | 12 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/DarkMode.ts | 12 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Header.ts | 69 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Media.ts | 153 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/MicMute.ts | 18 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Network.ts | 61 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/PowerProfile.ts | 99 | ||||
| -rw-r--r-- | .config/ags/widget/quicksettings/widgets/Volume.ts | 161 |
13 files changed, 1084 insertions, 0 deletions
diff --git a/.config/ags/widget/quicksettings/QuickSettings.ts b/.config/ags/widget/quicksettings/QuickSettings.ts new file mode 100644 index 0000000..2c0d6ac --- /dev/null +++ b/.config/ags/widget/quicksettings/QuickSettings.ts @@ -0,0 +1,84 @@ +import type Gtk from "gi://Gtk?version=3.0" +import { ProfileSelector, ProfileToggle } from "./widgets/PowerProfile" +import { Header } from "./widgets/Header" +import { Volume, Microhone, SinkSelector, AppMixer } from "./widgets/Volume" +import { Brightness } from "./widgets/Brightness" +import { NetworkToggle, WifiSelection } from "./widgets/Network" +import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth" +import { DND } from "./widgets/DND" +import { DarkModeToggle } from "./widgets/DarkMode" +import { MicMute } from "./widgets/MicMute" +import { Media } from "./widgets/Media" +import PopupWindow from "widget/PopupWindow" +import options from "options" + +const { bar, quicksettings } = options +const media = (await Service.import("mpris")).bind("players") +const layout = Utils.derive([bar.position, quicksettings.position], (bar, qs) => + `${bar}-${qs}` as const, +) + +const Row = ( + toggles: Array<() => Gtk.Widget> = [], + menus: Array<() => Gtk.Widget> = [], +) => Widget.Box({ + vertical: true, + children: [ + Widget.Box({ + homogeneous: true, + class_name: "row horizontal", + children: toggles.map(w => w()), + }), + ...menus.map(w => w()), + ], +}) + +const Settings = () => Widget.Box({ + vertical: true, + class_name: "quicksettings vertical", + css: quicksettings.width.bind().as(w => `min-width: ${w}px;`), + children: [ + Header(), + Widget.Box({ + class_name: "sliders-box vertical", + vertical: true, + children: [ + Row( + [Volume], + [SinkSelector, AppMixer], + ), + Microhone(), + Brightness(), + ], + }), + Row( + [NetworkToggle, BluetoothToggle], + [WifiSelection, BluetoothDevices], + ), + Row( + [ProfileToggle, DarkModeToggle], + [ProfileSelector], + ), + Row([MicMute, DND]), + Widget.Box({ + visible: media.as(l => l.length > 0), + child: Media(), + }), + ], +}) + +const QuickSettings = () => PopupWindow({ + name: "quicksettings", + exclusivity: "exclusive", + transition: bar.position.bind().as(pos => pos === "top" ? "slide_down" : "slide_up"), + layout: layout.value, + child: Settings(), +}) + +export function setupQuickSettings() { + App.addWindow(QuickSettings()) + layout.connect("changed", () => { + App.removeWindow("quicksettings") + App.addWindow(QuickSettings()) + }) +} diff --git a/.config/ags/widget/quicksettings/ToggleButton.ts b/.config/ags/widget/quicksettings/ToggleButton.ts new file mode 100644 index 0000000..62a2e67 --- /dev/null +++ b/.config/ags/widget/quicksettings/ToggleButton.ts @@ -0,0 +1,154 @@ +import { type Props as IconProps } from "types/widgets/icon" +import { type Props as LabelProps } from "types/widgets/label" +import type GObject from "gi://GObject?version=2.0" +import type Gtk from "gi://Gtk?version=3.0" +import icons from "lib/icons" + +export const opened = Variable("") +App.connect("window-toggled", (_, name: string, visible: boolean) => { + if (name === "quicksettings" && !visible) + Utils.timeout(500, () => opened.value = "") +}) + +export const Arrow = (name: string, activate?: false | (() => void)) => { + let deg = 0 + let iconOpened = false + const icon = Widget.Icon(icons.ui.arrow.right).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, + class_name: "arrow", + on_clicked: () => { + opened.value = opened.value === name ? "" : name + if (typeof activate === "function") + activate() + }, + }) +} + +type ArrowToggleButtonProps = { + name: string + icon: IconProps["icon"] + label: LabelProps["label"] + activate: () => void + deactivate: () => void + activateOnArrow?: boolean + connection: [GObject.Object, () => boolean] +} +export const ArrowToggleButton = ({ + name, + icon, + label, + activate, + deactivate, + activateOnArrow = true, + connection: [service, condition], +}: ArrowToggleButtonProps) => Widget.Box({ + class_name: "toggle-button", + setup: self => self.hook(service, () => { + self.toggleClassName("active", condition()) + }), + children: [ + Widget.Button({ + child: Widget.Box({ + hexpand: true, + children: [ + Widget.Icon({ + class_name: "icon", + icon, + }), + Widget.Label({ + class_name: "label", + max_width_chars: 10, + truncate: "end", + label, + }), + ], + }), + on_clicked: () => { + if (condition()) { + deactivate() + if (opened.value === name) + opened.value = "" + } else { + activate() + } + }, + }), + Arrow(name, activateOnArrow && activate), + ], +}) + +type MenuProps = { + name: string + icon: IconProps["icon"] + title: LabelProps["label"] + content: Gtk.Widget[] +} +export const Menu = ({ name, icon, title, content }: MenuProps) => Widget.Revealer({ + transition: "slide_down", + reveal_child: opened.bind().as(v => v === name), + child: Widget.Box({ + class_names: ["menu", name], + vertical: true, + children: [ + Widget.Box({ + class_name: "title-box", + children: [ + Widget.Icon({ + class_name: "icon", + icon, + }), + Widget.Label({ + class_name: "title", + truncate: "end", + label: title, + }), + ], + }), + Widget.Separator(), + Widget.Box({ + vertical: true, + class_name: "content vertical", + children: content, + }), + ], + }), +}) + +type SimpleToggleButtonProps = { + icon: IconProps["icon"] + label: LabelProps["label"] + toggle: () => void + connection: [GObject.Object, () => boolean] +} +export const SimpleToggleButton = ({ + icon, + label, + toggle, + connection: [service, condition], +}: SimpleToggleButtonProps) => Widget.Button({ + on_clicked: toggle, + class_name: "simple-toggle", + setup: self => self.hook(service, () => { + self.toggleClassName("active", condition()) + }), + child: Widget.Box([ + Widget.Icon({ icon }), + Widget.Label({ + max_width_chars: 10, + truncate: "end", + label, + }), + ]), +}) diff --git a/.config/ags/widget/quicksettings/quicksettings.scss b/.config/ags/widget/quicksettings/quicksettings.scss new file mode 100644 index 0000000..bd18ff1 --- /dev/null +++ b/.config/ags/widget/quicksettings/quicksettings.scss @@ -0,0 +1,177 @@ +window#quicksettings .quicksettings { + @include floating-widget; + @include spacing; + + padding: $popover-padding * 1.4; + + .avatar { + @include widget; + border-radius: $radius * 3; + } + + .header { + @include spacing(.5); + color: transparentize($fg, .15); + + button { + @include button; + padding: $padding; + + image { + font-size: 1.4em; + } + } + } + + .sliders-box { + @include widget; + padding: $padding; + + button { + @include button($flat: true); + padding: $padding * .5; + } + + .volume button.arrow:last-child { + margin-left: $spacing * .4; + } + + .volume, + .brightness { + padding: $padding * .5; + } + + scale { + @include slider; + margin: 0 ($spacing * .5); + + &.muted highlight { + background-image: none; + background-color: transparentize($fg, $amount: .2); + } + } + } + + .row { + @include spacing; + } + + .menu { + @include unset; + @include widget; + padding: $padding; + margin-top: $spacing; + + .icon { + margin: 0 ($spacing * .5); + margin-left: $spacing * .2; + } + + .title { + font-weight: bold; + } + + separator { + margin: ($radius * .5); + background-color: $border-color; + } + + button { + @include button($flat: true); + padding: ($padding * .5); + + image:first-child { + margin-right: $spacing * .5; + } + } + + .bluetooth-devices { + @include spacing(.5); + } + + switch { + @include switch; + } + } + + .sliders-box .menu { + margin: ($spacing * .5) 0; + + &.app-mixer { + .mixer-item { + padding: $padding * .5; + padding-left: 0; + padding-right: $padding * 2; + + scale { + @include slider($width: .5em); + } + + image { + font-size: 1.2em; + margin: 0 $padding; + } + } + } + } + + .toggle-button { + @include button; + font-weight: bold; + + image { + font-size: 1.3em; + } + + label { + margin-left: $spacing * .3; + } + + button { + @include button($flat: true); + + &:first-child { + padding: $padding * 1.2; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:last-child { + padding: $padding * .5; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + &.active { + background-color: $primary-bg; + + label, + image { + color: $primary-fg; + } + } + } + + .simple-toggle { + @include button; + font-weight: bold; + padding: $padding * 1.2; + + label { + margin-left: $spacing * .3; + } + + image { + font-size: 1.3em; + } + } + + .media { + @include spacing; + + .player { + @include media; + } + } +} diff --git a/.config/ags/widget/quicksettings/widgets/Bluetooth.ts b/.config/ags/widget/quicksettings/widgets/Bluetooth.ts new file mode 100644 index 0000000..649e654 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Bluetooth.ts @@ -0,0 +1,61 @@ +import { type BluetoothDevice } from "types/service/bluetooth" +import { Menu, ArrowToggleButton } from "../ToggleButton" +import icons from "lib/icons" + +const bluetooth = await Service.import("bluetooth") + +export const BluetoothToggle = () => ArrowToggleButton({ + name: "bluetooth", + icon: bluetooth.bind("enabled").as(p => icons.bluetooth[p ? "enabled" : "disabled"]), + label: Utils.watch("Disabled", bluetooth, () => { + if (!bluetooth.enabled) + return "Disabled" + + if (bluetooth.connected_devices.length === 1) + return bluetooth.connected_devices[0].alias + + return `${bluetooth.connected_devices.length} Connected` + }), + connection: [bluetooth, () => bluetooth.enabled], + deactivate: () => bluetooth.enabled = false, + activate: () => bluetooth.enabled = true, +}) + +const DeviceItem = (device: BluetoothDevice) => Widget.Box({ + children: [ + Widget.Icon(device.icon_name + "-symbolic"), + Widget.Label(device.name), + Widget.Label({ + label: `${device.battery_percentage}%`, + visible: device.bind("battery_percentage").as(p => p > 0), + }), + Widget.Box({ hexpand: true }), + Widget.Spinner({ + active: device.bind("connecting"), + visible: device.bind("connecting"), + }), + Widget.Switch({ + active: device.connected, + visible: device.bind("connecting").as(p => !p), + setup: self => self.on("notify::active", () => { + device.setConnection(self.active) + }), + }), + ], +}) + +export const BluetoothDevices = () => Menu({ + name: "bluetooth", + icon: icons.bluetooth.disabled, + title: "Bluetooth", + content: [ + Widget.Box({ + class_name: "bluetooth-devices", + hexpand: true, + vertical: true, + children: bluetooth.bind("devices").as(ds => ds + .filter(d => d.name) + .map(DeviceItem)), + }), + ], +}) diff --git a/.config/ags/widget/quicksettings/widgets/Brightness.ts b/.config/ags/widget/quicksettings/widgets/Brightness.ts new file mode 100644 index 0000000..a3ce565 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Brightness.ts @@ -0,0 +1,23 @@ +import icons from "lib/icons" +import brightness from "service/brightness" + +const BrightnessSlider = () => Widget.Slider({ + draw_value: false, + hexpand: true, + value: brightness.bind("screen"), + on_change: ({ value }) => brightness.screen = value, +}) + +export const Brightness = () => Widget.Box({ + class_name: "brightness", + children: [ + Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.brightness.indicator), + on_clicked: () => brightness.screen = 0, + tooltip_text: brightness.bind("screen").as(v => + `Screen Brightness: ${Math.floor(v * 100)}%`), + }), + BrightnessSlider(), + ], +}) diff --git a/.config/ags/widget/quicksettings/widgets/DND.ts b/.config/ags/widget/quicksettings/widgets/DND.ts new file mode 100644 index 0000000..7fc1fd0 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/DND.ts @@ -0,0 +1,12 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" + +const n = await Service.import("notifications") +const dnd = n.bind("dnd") + +export const DND = () => SimpleToggleButton({ + icon: dnd.as(dnd => icons.notifications[dnd ? "silent" : "noisy"]), + label: dnd.as(dnd => dnd ? "Silent" : "Noisy"), + toggle: () => n.dnd = !n.dnd, + connection: [n, () => n.dnd], +}) diff --git a/.config/ags/widget/quicksettings/widgets/DarkMode.ts b/.config/ags/widget/quicksettings/widgets/DarkMode.ts new file mode 100644 index 0000000..9ec94df --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/DarkMode.ts @@ -0,0 +1,12 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" +import options from "options" + +const { scheme } = options.theme + +export const DarkModeToggle = () => SimpleToggleButton({ + icon: scheme.bind().as(s => icons.color[s]), + label: scheme.bind().as(s => s === "dark" ? "Dark" : "Light"), + toggle: () => scheme.value = scheme.value === "dark" ? "light" : "dark", + connection: [scheme, () => scheme.value === "dark"], +}) diff --git a/.config/ags/widget/quicksettings/widgets/Header.ts b/.config/ags/widget/quicksettings/widgets/Header.ts new file mode 100644 index 0000000..44c26f2 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Header.ts @@ -0,0 +1,69 @@ +import icons from "lib/icons" +import { uptime } from "lib/variables" +import options from "options" +import powermenu, { Action } from "service/powermenu" + +const battery = await Service.import("battery") +const { image, size } = options.quicksettings.avatar + +function up(up: number) { + const h = Math.floor(up / 60) + const m = Math.floor(up % 60) + return `${h}h ${m < 10 ? "0" + m : m}m` +} + +const Avatar = () => Widget.Box({ + class_name: "avatar", + css: Utils.merge([image.bind(), size.bind()], (img, size) => ` + min-width: ${size}px; + min-height: ${size}px; + background-image: url('${img}'); + background-size: cover; + `), +}) + +const SysButton = (action: Action) => Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.powermenu[action]), + on_clicked: () => powermenu.action(action), +}) + +export const Header = () => Widget.Box( + { class_name: "header horizontal" }, + Avatar(), + Widget.Box({ + vertical: true, + vpack: "center", + children: [ + Widget.Box({ + visible: battery.bind("available"), + children: [ + Widget.Icon({ icon: battery.bind("icon_name") }), + Widget.Label({ label: battery.bind("percent").as(p => `${p}%`) }), + ], + }), + Widget.Box([ + Widget.Icon({ icon: icons.ui.time }), + Widget.Label({ label: uptime.bind().as(up) }), + + //Widget.Label({ label: `${user.name}\n` }), + // //Widget.Label({ label: uptime.bind().value }), + // Widget.Label({ label: `${user.name}\n ${uptime.bind().value}` }), + // //Widget.Icon({ icon: icons.ui.time }), + ]), + + ], + }), + Widget.Box({ hexpand: true }), + Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.ui.settings), + on_clicked: () => { + App.closeWindow("quicksettings") + App.closeWindow("settings-dialog") + App.openWindow("settings-dialog") + }, + }), + SysButton("logout"), + SysButton("shutdown"), +) diff --git a/.config/ags/widget/quicksettings/widgets/Media.ts b/.config/ags/widget/quicksettings/widgets/Media.ts new file mode 100644 index 0000000..52254ea --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Media.ts @@ -0,0 +1,153 @@ +import { type MprisPlayer } from "types/service/mpris" +import icons from "lib/icons" +import options from "options" +import { icon } from "lib/utils" + +const mpris = await Service.import("mpris") +const players = mpris.bind("players") +const { media } = options.quicksettings + +function lengthStr(length: number) { + const min = Math.floor(length / 60) + const sec = Math.floor(length % 60) + const sec0 = sec < 10 ? "0" : "" + return `${min}:${sec0}${sec}` +} + +const Player = (player: MprisPlayer) => { + const cover = Widget.Box({ + class_name: "cover", + vpack: "start", + css: Utils.merge([ + player.bind("cover_path"), + player.bind("track_cover_url"), + media.coverSize.bind(), + ], (path, url, size) => ` + min-width: ${size}px; + min-height: ${size}px; + background-image: url('${path || url}'); + `), + }) + + const title = Widget.Label({ + class_name: "title", + max_width_chars: 20, + truncate: "end", + hpack: "start", + label: player.bind("track_title"), + }) + + const artist = Widget.Label({ + class_name: "artist", + max_width_chars: 20, + truncate: "end", + hpack: "start", + label: player.bind("track_artists").as(a => a.join(", ")), + }) + + const positionSlider = Widget.Slider({ + class_name: "position", + draw_value: false, + on_change: ({ value }) => player.position = value * player.length, + setup: self => { + const update = () => { + const { length, position } = player + self.visible = length > 0 + self.value = length > 0 ? position / length : 0 + } + self.hook(player, update) + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const positionLabel = Widget.Label({ + class_name: "position", + hpack: "start", + setup: self => { + const update = (_: unknown, time?: number) => { + self.label = lengthStr(time || player.position) + self.visible = player.length > 0 + } + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const lengthLabel = Widget.Label({ + class_name: "length", + hpack: "end", + visible: player.bind("length").as(l => l > 0), + label: player.bind("length").as(lengthStr), + }) + + const playericon = Widget.Icon({ + class_name: "icon", + hexpand: true, + hpack: "end", + vpack: "start", + tooltip_text: player.identity || "", + icon: Utils.merge([player.bind("entry"), media.monochromeIcon.bind()], (e, s) => { + const name = `${e}${s ? "-symbolic" : ""}` + return icon(name, icons.fallback.audio) + }), + }) + + const playPause = Widget.Button({ + class_name: "play-pause", + on_clicked: () => player.playPause(), + visible: player.bind("can_play"), + child: Widget.Icon({ + icon: player.bind("play_back_status").as(s => { + switch (s) { + case "Playing": return icons.mpris.playing + case "Paused": + case "Stopped": return icons.mpris.stopped + } + }), + }), + }) + + const prev = Widget.Button({ + on_clicked: () => player.previous(), + visible: player.bind("can_go_prev"), + child: Widget.Icon(icons.mpris.prev), + }) + + const next = Widget.Button({ + on_clicked: () => player.next(), + visible: player.bind("can_go_next"), + child: Widget.Icon(icons.mpris.next), + }) + + return Widget.Box( + { class_name: "player", vexpand: false }, + cover, + Widget.Box( + { vertical: true }, + Widget.Box([ + title, + playericon, + ]), + artist, + Widget.Box({ vexpand: true }), + positionSlider, + Widget.CenterBox({ + class_name: "footer horizontal", + start_widget: positionLabel, + center_widget: Widget.Box([ + prev, + playPause, + next, + ]), + end_widget: lengthLabel, + }), + ), + ) +} + +export const Media = () => Widget.Box({ + vertical: true, + class_name: "media vertical", + children: players.as(p => p.map(Player)), +}) diff --git a/.config/ags/widget/quicksettings/widgets/MicMute.ts b/.config/ags/widget/quicksettings/widgets/MicMute.ts new file mode 100644 index 0000000..b6e9454 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/MicMute.ts @@ -0,0 +1,18 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" +const { microphone } = await Service.import("audio") + +const icon = () => microphone.is_muted || microphone.stream?.is_muted + ? icons.audio.mic.muted + : icons.audio.mic.high + +const label = () => microphone.is_muted || microphone.stream?.is_muted + ? "Muted" + : "Unmuted" + +export const MicMute = () => SimpleToggleButton({ + icon: Utils.watch(icon(), microphone, icon), + label: Utils.watch(label(), microphone, label), + toggle: () => microphone.is_muted = !microphone.is_muted, + connection: [microphone, () => microphone?.is_muted || false], +}) diff --git a/.config/ags/widget/quicksettings/widgets/Network.ts b/.config/ags/widget/quicksettings/widgets/Network.ts new file mode 100644 index 0000000..eb14ab4 --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Network.ts @@ -0,0 +1,61 @@ +import { Menu, ArrowToggleButton } from "../ToggleButton" +import icons from "lib/icons.js" +import { dependencies, sh } from "lib/utils" +import options from "options" +const { wifi } = await Service.import("network") + +export const NetworkToggle = () => ArrowToggleButton({ + name: "network", + icon: wifi.bind("icon_name"), + label: wifi.bind("ssid").as(ssid => ssid || "Not Connected"), + connection: [wifi, () => wifi.enabled], + deactivate: () => wifi.enabled = false, + activate: () => { + wifi.enabled = true + wifi.scan() + }, +}) + +export const WifiSelection = () => Menu({ + name: "network", + icon: wifi.bind("icon_name"), + title: "Wifi Selection", + content: [ + Widget.Box({ + vertical: true, + setup: self => self.hook(wifi, () => self.children = + wifi.access_points.map(ap => Widget.Button({ + on_clicked: () => { + if (dependencies("nmcli")) + Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`) + }, + child: Widget.Box({ + children: [ + Widget.Icon(ap.iconName), + Widget.Label(ap.ssid || ""), + Widget.Icon({ + icon: icons.ui.tick, + hexpand: true, + hpack: "end", + setup: self => Utils.idle(() => { + if (!self.is_destroyed) + self.visible = ap.active + }), + }), + ], + }), + })), + ), + }), + Widget.Separator(), + Widget.Button({ + on_clicked: () => sh(options.quicksettings.networkSettings.value), + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Network"), + ], + }), + }), + ], +}) diff --git a/.config/ags/widget/quicksettings/widgets/PowerProfile.ts b/.config/ags/widget/quicksettings/widgets/PowerProfile.ts new file mode 100644 index 0000000..f566aaf --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/PowerProfile.ts @@ -0,0 +1,99 @@ +import { ArrowToggleButton, Menu } from "../ToggleButton" +import icons from "lib/icons" + +import asusctl from "service/asusctl" +const asusprof = asusctl.bind("profile") + +const AsusProfileToggle = () => ArrowToggleButton({ + name: "asusctl-profile", + icon: asusprof.as(p => icons.asusctl.profile[p]), + label: asusprof, + connection: [asusctl, () => asusctl.profile !== "Balanced"], + activate: () => asusctl.setProfile("Quiet"), + deactivate: () => asusctl.setProfile("Balanced"), + activateOnArrow: false, +}) + +const AsusProfileSelector = () => Menu({ + name: "asusctl-profile", + icon: asusprof.as(p => icons.asusctl.profile[p]), + title: "Profile Selector", + content: [ + Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + vertical: true, + children: asusctl.profiles.map(prof => Widget.Button({ + on_clicked: () => asusctl.setProfile(prof), + child: Widget.Box({ + children: [ + Widget.Icon(icons.asusctl.profile[prof]), + Widget.Label(prof), + ], + }), + })), + }), + ], + }), + Widget.Separator(), + Widget.Button({ + on_clicked: () => Utils.execAsync("rog-control-center"), + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Rog Control Center"), + ], + }), + }), + ], +}) + + +const pp = await Service.import("powerprofiles") +const profile = pp.bind("active_profile") +const profiles = pp.profiles.map(p => p.Profile) + +const pretty = (str: string) => str + .split("-") + .map(str => `${str.at(0)?.toUpperCase()}${str.slice(1)}`) + .join(" ") + +const PowerProfileToggle = () => ArrowToggleButton({ + name: "asusctl-profile", + icon: profile.as(p => icons.powerprofile[p]), + label: profile.as(pretty), + connection: [pp, () => pp.active_profile !== profiles[1]], + activate: () => pp.active_profile = profiles[0], + deactivate: () => pp.active_profile = profiles[1], + activateOnArrow: false, +}) + +const PowerProfileSelector = () => Menu({ + name: "asusctl-profile", + icon: profile.as(p => icons.powerprofile[p]), + title: "Profile Selector", + content: [Widget.Box({ + vertical: true, + hexpand: true, + child: Widget.Box({ + vertical: true, + children: profiles.map(prof => Widget.Button({ + on_clicked: () => pp.active_profile = prof, + child: Widget.Box({ + children: [ + Widget.Icon(icons.powerprofile[prof]), + Widget.Label(pretty(prof)), + ], + }), + })), + }), + })], +}) + +export const ProfileToggle = asusctl.available + ? AsusProfileToggle : PowerProfileToggle + +export const ProfileSelector = asusctl.available + ? AsusProfileSelector : PowerProfileSelector diff --git a/.config/ags/widget/quicksettings/widgets/Volume.ts b/.config/ags/widget/quicksettings/widgets/Volume.ts new file mode 100644 index 0000000..077439a --- /dev/null +++ b/.config/ags/widget/quicksettings/widgets/Volume.ts @@ -0,0 +1,161 @@ +import { type Stream } from "types/service/audio" +import { Arrow, Menu } from "../ToggleButton" +import { dependencies, icon, sh } from "lib/utils" +import icons from "lib/icons.js" +const audio = await Service.import("audio") + +type Type = "microphone" | "speaker" + +const VolumeIndicator = (type: Type = "speaker") => Widget.Button({ + vpack: "center", + on_clicked: () => audio[type].is_muted = !audio[type].is_muted, + child: Widget.Icon({ + icon: audio[type].bind("icon_name") + .as(i => icon(i || "", icons.audio.volume.medium)), + tooltipText: audio[type].bind("volume") + .as(vol => `Volume: ${Math.floor(vol * 100)}%`), + }), +}) + +const micIndicator = (type: Type = "microphone") => Widget.Button({ + vpack: "center", + on_clicked: () => audio[type].is_muted = !audio[type].is_muted, + child: Widget.Icon({ + icon: audio[type].bind("icon_name") + .as(i => icon(i || "", icons.audio.mic.medium)), + tooltipText: audio[type].bind("volume") + .as(vol => `Volume: ${Math.floor(vol * 100)}%`), + }), +}) + +const VolumeSlider = (type: Type = "speaker") => Widget.Slider({ + hexpand: true, + draw_value: false, + on_change: ({ value, dragging }) => { + if (dragging) { + audio[type].volume = value + audio[type].is_muted = false + } + }, + value: audio[type].bind("volume"), + class_name: audio[type].bind("is_muted").as(m => m ? "muted" : ""), +}) + +export const Volume = () => Widget.Box({ + class_name: "volume", + children: [ + VolumeIndicator("speaker"), + VolumeSlider("speaker"), + Widget.Box({ + vpack: "center", + child: Arrow("sink-selector"), + }), + Widget.Box({ + vpack: "center", + child: Arrow("app-mixer"), + visible: audio.bind("apps").as(a => a.length > 0), + }), + ], +}) + +export const Microhone = () => Widget.Box({ + class_name: "slider horizontal", + visible: audio.bind("recorders").as(a => a.length > 0), + children: [ + micIndicator("microphone"), + VolumeSlider("microphone"), + ], +}) + +const MixerItem = (stream: Stream) => Widget.Box( + { + hexpand: true, + class_name: "mixer-item horizontal", + }, + Widget.Icon({ + tooltip_text: stream.bind("name").as(n => n || ""), + icon: stream.bind("name").as(n => { + return Utils.lookUpIcon(n || "") + ? (n || "") + : icons.fallback.audio + }), + }), + Widget.Box( + { vertical: true }, + Widget.Label({ + xalign: 0, + truncate: "end", + max_width_chars: 28, + label: stream.bind("description").as(d => d || ""), + }), + Widget.Slider({ + hexpand: true, + draw_value: false, + value: stream.bind("volume"), + on_change: ({ value }) => stream.volume = value, + }), + ), +) + +const SinkItem = (stream: Stream) => Widget.Button({ + hexpand: true, + on_clicked: () => audio.speaker = stream, + child: Widget.Box({ + children: [ + Widget.Icon({ + icon: icon(stream.icon_name || "", icons.fallback.audio), + tooltip_text: stream.icon_name || "", + }), + Widget.Label((stream.description || "").split(" ").slice(0, 4).join(" ")), + Widget.Icon({ + icon: icons.ui.tick, + hexpand: true, + hpack: "end", + visible: audio.speaker.bind("stream").as(s => s === stream.stream), + }), + ], + }), +}) + +const SettingsButton = () => Widget.Button({ + on_clicked: () => { + if (dependencies("pavucontrol")) + sh("pavucontrol") + }, + hexpand: true, + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Settings"), + ], + }), +}) + +export const AppMixer = () => Menu({ + name: "app-mixer", + icon: icons.audio.mixer, + title: "App Mixer", + content: [ + Widget.Box({ + vertical: true, + class_name: "vertical mixer-item-box", + children: audio.bind("apps").as(a => a.map(MixerItem)), + }), + Widget.Separator(), + SettingsButton(), + ], +}) + +export const SinkSelector = () => Menu({ + name: "sink-selector", + icon: icons.audio.type.headset, + title: "Sink Selector", + content: [ + Widget.Box({ + vertical: true, + children: audio.bind("speakers").as(a => a.map(SinkItem)), + }), + Widget.Separator(), + SettingsButton(), + ], +}) |
