From 936e9aacb03e4c99f6d7b77424b83baf7a30035f Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Sat, 7 Dec 2024 14:17:37 -0500
Subject: [PATCH] feat(ags): add start of audio widget

---
 .../ags/config/configurations/binto.ts        |   2 +
 nixosModules/ags/config/configurations/wim.ts |   2 +
 nixosModules/ags/config/style/common.scss     |   8 ++
 nixosModules/ags/config/style/main.scss       |   1 +
 .../ags/config/widgets/audio/_index.scss      |  11 ++
 .../ags/config/widgets/audio/binto.tsx        |  18 +++
 .../ags/config/widgets/audio/main.tsx         | 111 ++++++++++++++++++
 nixosModules/ags/config/widgets/audio/wim.tsx |  15 +++
 .../ags/config/widgets/bar/items/audio.tsx    |  24 +++-
 .../ags/config/widgets/misc/subclasses.tsx    |  24 +++-
 .../config/widgets/on-screen-display/main.tsx |  15 +--
 11 files changed, 214 insertions(+), 17 deletions(-)
 create mode 100644 nixosModules/ags/config/widgets/audio/_index.scss
 create mode 100644 nixosModules/ags/config/widgets/audio/binto.tsx
 create mode 100644 nixosModules/ags/config/widgets/audio/main.tsx
 create mode 100644 nixosModules/ags/config/widgets/audio/wim.tsx

diff --git a/nixosModules/ags/config/configurations/binto.ts b/nixosModules/ags/config/configurations/binto.ts
index 0dae2a28..3c6da082 100644
--- a/nixosModules/ags/config/configurations/binto.ts
+++ b/nixosModules/ags/config/configurations/binto.ts
@@ -4,6 +4,7 @@ import { App } from 'astal/gtk3';
 import style from '../style/main.scss';
 
 import AppLauncher from '../widgets/applauncher/main';
+import AudioWindow from '../widgets/audio/binto';
 import Bar from '../widgets/bar/binto';
 import BgLayer from '../widgets/bg-layer/main';
 import Calendar from '../widgets/date/binto';
@@ -52,6 +53,7 @@ export default () => {
             perMonitor((monitor) => BgLayer(monitor, false));
 
             AppLauncher();
+            AudioWindow();
             Bar();
             Calendar();
             Clipboard();
diff --git a/nixosModules/ags/config/configurations/wim.ts b/nixosModules/ags/config/configurations/wim.ts
index c4be9d55..b7b44f56 100644
--- a/nixosModules/ags/config/configurations/wim.ts
+++ b/nixosModules/ags/config/configurations/wim.ts
@@ -4,6 +4,7 @@ import { App } from 'astal/gtk3';
 import style from '../style/main.scss';
 
 import AppLauncher from '../widgets/applauncher/main';
+import AudioWindow from '../widgets/audio/wim';
 import Bar from '../widgets/bar/wim';
 import BgLayer from '../widgets/bg-layer/main';
 import BluetoothWindow from '../widgets/bluetooth/wim';
@@ -55,6 +56,7 @@ export default () => {
             perMonitor((monitor) => BgLayer(monitor, true));
 
             AppLauncher();
+            AudioWindow();
             Bar();
             BluetoothWindow();
             Calendar();
diff --git a/nixosModules/ags/config/style/common.scss b/nixosModules/ags/config/style/common.scss
index b89ec0ea..4d20f311 100644
--- a/nixosModules/ags/config/style/common.scss
+++ b/nixosModules/ags/config/style/common.scss
@@ -30,6 +30,14 @@ progressbar {
     }
 }
 
+scale {
+    contents {
+        trough {
+            min-height: 10px;
+        }
+    }
+}
+
 circular-progress {
     background: #363847;
     min-height: 35px;
diff --git a/nixosModules/ags/config/style/main.scss b/nixosModules/ags/config/style/main.scss
index d31e20cd..39045afb 100644
--- a/nixosModules/ags/config/style/main.scss
+++ b/nixosModules/ags/config/style/main.scss
@@ -1,6 +1,7 @@
 @use 'common';
 
 @use '../widgets/applauncher';
+@use '../widgets/audio';
 @use '../widgets/bar';
 @use '../widgets/bluetooth';
 @use '../widgets/clipboard';
diff --git a/nixosModules/ags/config/widgets/audio/_index.scss b/nixosModules/ags/config/widgets/audio/_index.scss
new file mode 100644
index 00000000..49fe8ab4
--- /dev/null
+++ b/nixosModules/ags/config/widgets/audio/_index.scss
@@ -0,0 +1,11 @@
+@use 'sass:color';
+@use '../../style/colors';
+
+.widget.audio {
+    .stream {
+        margin: 5px;
+        padding: 10px;
+        border-radius: 8px;
+        background-color: color.adjust(colors.$window_bg_color, $lightness: -3%);
+    }
+}
diff --git a/nixosModules/ags/config/widgets/audio/binto.tsx b/nixosModules/ags/config/widgets/audio/binto.tsx
new file mode 100644
index 00000000..4be56ddf
--- /dev/null
+++ b/nixosModules/ags/config/widgets/audio/binto.tsx
@@ -0,0 +1,18 @@
+import { Astal } from 'astal/gtk3';
+
+import PopupWindow from '../misc/popup-window';
+import { get_gdkmonitor_from_desc } from '../../lib';
+
+import AudioWidget from './main';
+
+
+export default () => (
+    <PopupWindow
+        name="audio"
+        gdkmonitor={get_gdkmonitor_from_desc('desc:Acer Technologies Acer K212HQL T3EAA0014201')}
+        anchor={Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.TOP}
+        transition="slide bottom"
+    >
+        <AudioWidget />
+    </PopupWindow>
+);
diff --git a/nixosModules/ags/config/widgets/audio/main.tsx b/nixosModules/ags/config/widgets/audio/main.tsx
new file mode 100644
index 00000000..277f481e
--- /dev/null
+++ b/nixosModules/ags/config/widgets/audio/main.tsx
@@ -0,0 +1,111 @@
+import { bind } from 'astal';
+import { Gtk, Widget } from 'astal/gtk3';
+
+import AstalWp from 'gi://AstalWp';
+
+import { RadioButton, ToggleButton } from '../misc/subclasses';
+import Separator from '../misc/separator';
+
+
+export default () => {
+    const audio = AstalWp.get_default()?.get_audio();
+
+    if (!audio) {
+        throw new Error('Could not find default audio devices.');
+    }
+
+    // TODO: make a stack to have outputs, inputs and currently playing apps
+    // TODO: figure out ports and profiles
+
+    const defaultGroup = new RadioButton();
+
+    return (
+        <box vertical className="widget audio">
+            {bind(audio, 'speakers').as((speakers) => speakers.map((speaker) => (
+                <box className="stream" vertical>
+                    <box className="title">
+                        <RadioButton
+                            css="margin-top: 1px;"
+
+                            group={defaultGroup}
+                            active={speaker.isDefault}
+
+                            setup={(self) => {
+                                speaker.connect('notify::isDefault', () => {
+                                    self.active = speaker.isDefault;
+                                });
+                            }}
+
+                            onToggled={(self) => {
+                                speaker.isDefault = self.active;
+                            }}
+                        />
+
+                        <Separator size={8} />
+
+                        <label label={bind(speaker, 'description')} />
+                    </box>
+
+                    <Separator size={4} vertical />
+
+                    <box className="body">
+                        <ToggleButton
+                            cursor="pointer"
+                            valign={Gtk.Align.END}
+
+                            active={speaker.mute}
+                            onToggled={(self) => {
+                                speaker.set_mute(self.active);
+
+                                (self.get_child() as Widget.Icon).icon = self.active ?
+                                    'audio-volume-muted-symbolic' :
+                                    'audio-speakers-symbolic';
+                            }}
+                        >
+                            <icon icon={speaker.mute ?
+                                'audio-volume-muted-symbolic' :
+                                'audio-speakers-symbolic'}
+                            />
+                        </ToggleButton>
+
+                        <Separator size={4} />
+
+                        {/*
+                            FIXME: lockChannels not working
+                            TODO: have two sliders when lockChannels === false
+                        */}
+                        <ToggleButton
+                            cursor="pointer"
+                            valign={Gtk.Align.END}
+
+                            active={speaker.lockChannels}
+                            onToggled={(self) => {
+                                speaker.set_lock_channels(self.active);
+
+                                (self.get_child() as Widget.Icon).icon = self.active ?
+                                    'channel-secure-symbolic' :
+                                    'channel-insecure-symbolic';
+                            }}
+                        >
+                            <icon icon={speaker.lockChannels ?
+                                'channel-secure-symbolic' :
+                                'channel-insecure-symbolic'}
+                            />
+                        </ToggleButton>
+
+                        <slider
+                            hexpand
+                            halign={Gtk.Align.FILL}
+                            drawValue
+
+                            value={bind(speaker, 'volume')}
+                            onDragged={(self) => {
+                                speaker.set_volume(self.value);
+                            }}
+                        />
+                    </box>
+                </box>
+            )))}
+        </box>
+    );
+};
diff --git a/nixosModules/ags/config/widgets/audio/wim.tsx b/nixosModules/ags/config/widgets/audio/wim.tsx
new file mode 100644
index 00000000..b3327738
--- /dev/null
+++ b/nixosModules/ags/config/widgets/audio/wim.tsx
@@ -0,0 +1,15 @@
+import { Astal } from 'astal/gtk3';
+
+import PopupWindow from '../misc/popup-window';
+
+import AudioWidget from './main';
+
+
+export default () => (
+    <PopupWindow
+        name="audio"
+        anchor={Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.TOP}
+    >
+        <AudioWidget />
+    </PopupWindow>
+);
diff --git a/nixosModules/ags/config/widgets/bar/items/audio.tsx b/nixosModules/ags/config/widgets/bar/items/audio.tsx
index 63b3fcc6..a3e2a619 100644
--- a/nixosModules/ags/config/widgets/bar/items/audio.tsx
+++ b/nixosModules/ags/config/widgets/bar/items/audio.tsx
@@ -1,7 +1,11 @@
 import { bind } from 'astal';
+import { App } from 'astal/gtk3';
 
 import AstalWp from 'gi://AstalWp';
 
+import PopupWindow from '../../misc/popup-window';
+
+
 export default () => {
     const speaker = AstalWp.get_default()?.audio.default_speaker;
 
@@ -10,8 +14,22 @@ export default () => {
     }
 
     return (
-        <box className="bar-item audio">
-            <overlay>
+        <button
+            cursor="pointer"
+            className="bar-item audio"
+
+            onButtonReleaseEvent={(self) => {
+                const win = App.get_window('win-audio') as PopupWindow;
+
+                win.set_x_pos(
+                    self.get_allocation(),
+                    'right',
+                );
+
+                win.visible = !win.visible;
+            }}
+        >
+            <overlay passThrough>
                 <circularprogress
                     startAt={0.75}
                     endAt={0.75}
@@ -23,6 +41,6 @@ export default () => {
 
                 <icon icon={bind(speaker, 'volumeIcon')} />
             </overlay>
-        </box>
+        </button>
     );
 };
diff --git a/nixosModules/ags/config/widgets/misc/subclasses.tsx b/nixosModules/ags/config/widgets/misc/subclasses.tsx
index 626448fc..59c5b596 100644
--- a/nixosModules/ags/config/widgets/misc/subclasses.tsx
+++ b/nixosModules/ags/config/widgets/misc/subclasses.tsx
@@ -13,6 +13,17 @@ export class ToggleButton extends astalify(Gtk.ToggleButton) {
     }
 }
 
+@register()
+export class RadioButton extends astalify(Gtk.RadioButton) {
+    constructor(props: ConstructProps<
+        RadioButton,
+        Gtk.RadioButton.ConstructorProps
+    > = {}) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        super(props as any);
+    }
+}
+
 @register()
 export class ListBox extends astalify(Gtk.ListBox) {
     override get_children() {
@@ -22,7 +33,18 @@ export class ListBox extends astalify(Gtk.ListBox) {
     constructor(props: ConstructProps<
         ListBox,
         Gtk.ListBox.ConstructorProps
-    >) {
+    > = {}) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        super(props as any);
+    }
+}
+
+@register()
+export class ProgressBar extends astalify(Gtk.ProgressBar) {
+    constructor(props: ConstructProps<
+        ProgressBar,
+        Gtk.ProgressBar.ConstructorProps
+    > = {}) {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         super(props as any);
     }
diff --git a/nixosModules/ags/config/widgets/on-screen-display/main.tsx b/nixosModules/ags/config/widgets/on-screen-display/main.tsx
index d0f161ba..23fbf15d 100644
--- a/nixosModules/ags/config/widgets/on-screen-display/main.tsx
+++ b/nixosModules/ags/config/widgets/on-screen-display/main.tsx
@@ -1,9 +1,9 @@
 import { bind, timeout } from 'astal';
-import { register } from 'astal/gobject';
-import { App, Astal, astalify, Gtk, Widget, type ConstructProps } from 'astal/gtk3';
+import { App, Astal, Gtk, Widget } from 'astal/gtk3';
 
 import AstalWp from 'gi://AstalWp';
 
+import { ProgressBar } from '../misc/subclasses';
 import PopupWindow from '../misc/popup-window';
 import Brightness from '../../services/brightness';
 
@@ -12,17 +12,6 @@ declare global {
     function popup_osd(osd: string): void;
 }
 
-@register()
-class ProgressBar extends astalify(Gtk.ProgressBar) {
-    constructor(props: ConstructProps<
-        ProgressBar,
-        Gtk.ProgressBar.ConstructorProps
-    >) {
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        super(props as any);
-    }
-}
-
 
 const HIDE_DELAY = 2000;
 const transition_duration = 300;