From c4b71fc0d41dc1ca4498a03e977f0d0be803a316 Mon Sep 17 00:00:00 2001
From: Nydragon <contact@ccnlc.eu>
Date: Fri, 15 Nov 2024 03:08:33 +0100
Subject: [PATCH] feat: add wip notifications to dash

---
 src/Dashboard.qml                             |  45 +++---
 src/MainBar.qml                               |   6 +-
 src/base/BRectangle.qml                       |   3 +-
 src/provider/Config.qml                       |  22 +++
 src/provider/Notifications.qml                |  15 +-
 src/provider/qmldir                           |   1 +
 src/utils/qmldir                              |   2 +
 src/utils/timer.mjs                           |  13 ++
 .../notifications/NotificationToast.qml}      | 135 ++++++++----------
 .../NotificationToastAction.qml}              |   0
 .../notificationtoast/NotificationToasts.qml  |  45 +++++-
 11 files changed, 184 insertions(+), 103 deletions(-)
 create mode 100644 src/provider/Config.qml
 create mode 100644 src/utils/qmldir
 create mode 100644 src/utils/timer.mjs
 rename src/{windows/notificationtoast/Toast.qml => widgets/notifications/NotificationToast.qml} (50%)
 rename src/{windows/notificationtoast/ToastAction.qml => widgets/notifications/NotificationToastAction.qml} (100%)

diff --git a/src/Dashboard.qml b/src/Dashboard.qml
index be04dad..55df69f 100644
--- a/src/Dashboard.qml
+++ b/src/Dashboard.qml
@@ -1,9 +1,11 @@
 import Quickshell
+import Quickshell.Services.Notifications
 import QtQuick
 import QtQuick.Layouts
 import QtQml
 import "base"
 import "widgets/mpris"
+import "widgets/notifications"
 import "provider"
 
 PanelWindow {
@@ -47,23 +49,6 @@ PanelWindow {
 
             Component.onCompleted: () => maxSize = homeWindow.screen.width * (2 / 7)
 
-            ListView {
-                width: parent.width
-                height: parent.height
-                model: Notifications.incoming
-
-                delegate: Rectangle {
-                    required property var modelData
-                    width: 100
-                    height: 50
-                    Text {
-                        text: parent.modelData.appName
-                        width: parent.width
-                        height: parent.height
-                    }
-                }
-            }
-
             Behavior on width {
                 PropertyAnimation {
                     id: anim
@@ -89,6 +74,32 @@ PanelWindow {
                     Layout.margins: 15
                     Layout.alignment: Qt.AlignBottom
 
+                    ListView {
+                        id: popupcol
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                        Layout.preferredHeight: 1000
+                        spacing: 10
+                        width: parent.width
+                        Component.onCompleted: () => {}
+
+                        model: Notifications.list
+
+                        delegate: NotificationToast {
+                            id: toast
+
+                            required property var modelData
+                            required property int index
+
+                            notif: modelData
+                            width: ListView.view.width
+
+                            onClose: {
+                                toast.notif.dismiss();
+                            }
+                        }
+                    }
+
                     MprisSmall {}
                 }
             }
diff --git a/src/MainBar.qml b/src/MainBar.qml
index a6df037..8398b2a 100644
--- a/src/MainBar.qml
+++ b/src/MainBar.qml
@@ -7,6 +7,7 @@ import "widgets/caffeine"
 import "windows/notificationtoast"
 import "windows/workspace-view"
 import "base"
+import "provider"
 import Quickshell // for ShellRoot and PanelWindow
 import QtQuick
 import QtQuick.Layouts
@@ -18,9 +19,11 @@ PanelWindow {
 
     anchors {
         top: true
-        left: true
+        left: Config.alignment === Config.BarAlignment.Left
+        right: Config.alignment === Config.BarAlignment.Right
         bottom: true
     }
+
     margins.left: 2
     margins.top: 2
     margins.bottom: 2
@@ -68,6 +71,7 @@ PanelWindow {
                 Layout.fillHeight: true
             }
         }
+
         MouseArea {
             id: mouse
             onClicked: lbar.root.enabled = !lbar.root.enabled
diff --git a/src/base/BRectangle.qml b/src/base/BRectangle.qml
index fe1196b..b36c5b1 100644
--- a/src/base/BRectangle.qml
+++ b/src/base/BRectangle.qml
@@ -1,4 +1,5 @@
 import QtQuick
+import "../provider"
 
 Rectangle {
     width: parent.width
@@ -6,5 +7,5 @@ Rectangle {
     border.color: "black"
     border.width: 1
     radius: 5
-    color: "#BD93F9"
+    color: Config.colours.main
 }
diff --git a/src/provider/Config.qml b/src/provider/Config.qml
new file mode 100644
index 0000000..c501f51
--- /dev/null
+++ b/src/provider/Config.qml
@@ -0,0 +1,22 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+
+Singleton {
+    id: config
+    property Item notifications: Item {
+        property int toastDuration: 5000
+    }
+
+    enum BarAlignment {
+        Left,
+        Right
+    }
+
+    property int alignment: Config.BarAlignment.Left
+
+    property Item colours: Item {
+        property color main: "#BD93F9"
+    }
+}
diff --git a/src/provider/Notifications.qml b/src/provider/Notifications.qml
index eea7a2a..fdc4a48 100644
--- a/src/provider/Notifications.qml
+++ b/src/provider/Notifications.qml
@@ -3,11 +3,13 @@ pragma Singleton
 import Quickshell.Services.Notifications
 import Quickshell
 import QtQuick
+import "../utils/timer.mjs" as Timer
 
 Singleton {
     id: notif
 
     property var _: NotificationServer {
+        id: server
         actionIconsSupported: true
         actionsSupported: true
         bodyHyperlinksSupported: true
@@ -18,10 +20,17 @@ Singleton {
 
         onNotification: n => {
             n.tracked = true;
-            incoming.push(n);
+
+            notif.incomingAdded(n);
+
+            Timer.after(1000, notif, () => {
+                notif.incomingRemoved(n.id);
+            });
         }
     }
 
-    property list<Notification> backlog: notif._.trackedNotifications
-    property list<Notification> incoming: []
+    property alias list: server.trackedNotifications
+
+    signal incomingRemoved(id: int)
+    signal incomingAdded(id: Notification)
 }
diff --git a/src/provider/qmldir b/src/provider/qmldir
index b12d3c7..c74f20d 100644
--- a/src/provider/qmldir
+++ b/src/provider/qmldir
@@ -1,4 +1,5 @@
 module Provider
+singleton Config 0.1 Config.qml
 singleton Player 0.1 Player.qml
 singleton Inhibitor 0.1 Inhibitor.qml
 singleton Notifications 0.1 Notifications.qml
diff --git a/src/utils/qmldir b/src/utils/qmldir
new file mode 100644
index 0000000..c826c8c
--- /dev/null
+++ b/src/utils/qmldir
@@ -0,0 +1,2 @@
+module Utils
+Timer 0.1 timer.mjs
diff --git a/src/utils/timer.mjs b/src/utils/timer.mjs
new file mode 100644
index 0000000..eab772c
--- /dev/null
+++ b/src/utils/timer.mjs
@@ -0,0 +1,13 @@
+export function Timer(parent) {
+  return Qt.createQmlObject("import QtQuick; Timer {}", parent);
+}
+
+export function after(delay, parent, callback) {
+  const timer = new Timer(parent);
+
+  timer.interval = delay;
+
+  timer.triggered.connect(callback);
+
+  timer.start();
+}
diff --git a/src/windows/notificationtoast/Toast.qml b/src/widgets/notifications/NotificationToast.qml
similarity index 50%
rename from src/windows/notificationtoast/Toast.qml
rename to src/widgets/notifications/NotificationToast.qml
index 9eae07b..e7b90c9 100644
--- a/src/windows/notificationtoast/Toast.qml
+++ b/src/widgets/notifications/NotificationToast.qml
@@ -1,40 +1,43 @@
 pragma ComponentBehavior: Bound
 import QtQuick
-import QtQuick.Controls
 import QtQuick.Layouts
+import QtQuick.Controls
 import Quickshell
 import Quickshell.Widgets
 import "../../base"
+import "../../provider"
 import Quickshell.Services.Notifications
 
 MouseArea {
     id: toast
-    readonly property int lifetime: 5000
-    property int countdownTime: lifetime
 
-    required property string appName
-    required property string summary
-    required property string body
-    required property string appIcon
-    required property string image
-    required property NotificationUrgency urgency
-    required property bool hasActionIcons
-    required property var actions
-    required property int index
-    Component.onCompleted: () => console.log(toast.actions.values)
+    required property Notification notif
 
-    function close(): void {
-        popupcol.model.remove(toast.index, 1);
+    property int actionHeight: 30
+    property int expansionSpeed: 200
+    property bool showTimeBar: false
+
+    property string body
+    property string appName
+    property string summary
+    property string appIcon
+
+    Component.onCompleted: {
+        body = notif?.body ?? "";
+        appName = notif?.appName ?? "";
+        summary = notif?.summary ?? "";
+        appIcon = notif?.appIcon ?? "";
     }
 
-    hoverEnabled: true
     height: box.height
-    width: popupcol.width
+    hoverEnabled: true
+
+    signal close
 
     BRectangle {
         id: box
         width: parent.width
-        height: header.height + actions.height + test.height + (5 * 3)
+        height: header.height + toast.actionHeight + bodyBox.height + (5 * 3)
 
         clip: true
 
@@ -52,29 +55,25 @@ MouseArea {
                     source: toast.appIcon ? Quickshell.iconPath(toast.appIcon) : ""
                     height: parent.height
                     width: height
-                    visible: toast.appIcon
+                    visible: toast.appIcon ?? false
                 }
 
                 Text {
-                    text: (toast.appIcon ? " " : toast.appName + ": ") + toast.summary
+                    text: `${toast.appIcon ? "" : `${toast.appName}:`} ${toast.summary}`
                     Layout.fillWidth: true
                     elide: Text.ElideRight
                     font.pointSize: 12.5
                 }
 
-                Item {
-                    Layout.fillHeight: true
-                    Layout.rightMargin: 16
-                    Button {
-                        onClicked: toast.close()
-                        height: 16
-                        width: 16
-                    }
+                Button {
+                    onClicked: toast.close()
+                    height: 16
+                    width: 16
                 }
             }
 
             Rectangle {
-                id: test
+                id: bodyBox
                 width: parent.width
                 anchors.top: header.bottom
                 height: 60
@@ -85,17 +84,19 @@ MouseArea {
                 Text {
                     id: text
                     anchors.topMargin: 5
-                    text: toast.body
+                    text: toast.body ?? ""
                     width: parent.width
                     height: parent.height
                     wrapMode: Text.Wrap
                     elide: Text.ElideRight
                     font.pointSize: 12.5
+
                     Component.onCompleted: () => {
-                        if (text.implicitHeight < test.height) {
-                            test.height = text.implicitHeight;
+                        if (text.implicitHeight < bodyBox.height) {
+                            bodyBox.height = text.implicitHeight;
                         }
-                        test.maxHeight = text.implicitHeight;
+
+                        bodyBox.maxHeight = Qt.binding(() => text.implicitHeight);
                     }
                 }
 
@@ -103,67 +104,55 @@ MouseArea {
                     name: "expand"
                     when: toast.containsMouse
                     PropertyChanges {
-                        target: test
-                        height: test.maxHeight
+                        target: bodyBox
+                        height: bodyBox.maxHeight
                     }
                 }
 
                 transitions: Transition {
                     NumberAnimation {
-                        properties: "width,height"
-                        duration: 50
-                        easing.type: Easing.InOutQuad
+                        properties: "height"
+                        duration: toast.expansionSpeed
                     }
                 }
             }
-
             RowLayout {
                 id: actions
                 width: parent.width
-                anchors.top: test.bottom
+                anchors.top: bodyBox.bottom
                 anchors.topMargin: 5
                 anchors.bottomMargin: 5
                 Repeater {
-                    model: toast.actions
+                    id: rep
+                    model: toast.notif?.actions
 
-                    delegate: ToastAction {
+                    delegate: NotificationToastAction {
                         required property var modelData
                         notifAction: modelData
-                        hasIcons: toast.hasActionIcons
+                        hasIcons: toast.notif?.hasActionIcons ?? false
+                        height: toast.actionHeight
                     }
                 }
-
-                visible: toast?.actions ? true : false
-            }
-
-            states: State {
-                name: "expand"
-                when: toast.containsMouse
-                PropertyChanges {
-                    target: box
-                    height: test.height + header.height + actions.height + 15
-                }
-            }
-
-            transitions: Transition {
-                NumberAnimation {
-                    properties: "width,height"
-                    duration: 50
-                    easing.type: Easing.InOutQuad
-                }
+                visible: toast.notif?.actions.length ?? false
             }
         }
 
+        NumberAnimation on width {
+            duration: toast.expansionSpeed
+        }
+
         Rectangle {
+            id: timeBar
+            visible: toast.showTimeBar
             anchors.margins: 2
             anchors.bottom: box.bottom
             anchors.right: box.right
-            width: (box.width - box.border.width - anchors.margins) * (toast.countdownTime / toast.lifetime)
+            width: box.width - box.border.width - anchors.margins
             height: 2
             bottomLeftRadius: box.radius
             bottomRightRadius: box.radius
             color: {
-                switch (toast.urgency) {
+                switch (toast.notif?.urgency) {
                 case NotificationUrgency.Critical:
                     return "red";
                     break;
@@ -174,21 +163,13 @@ MouseArea {
                     return "white";
                 }
             }
-        }
-    }
 
-    Timer {
-        id: timer
-        interval: 10
-        repeat: !toast.containsMouse
-        onTriggered: () => {
-            toast.countdownTime -= timer.interval;
-            if (toast.countdownTime <= 0) {
-                toast.parent.parent.model.remove(toast.index, 1);
-                timer.repeat = false;
-                timer.running = false;
+            NumberAnimation on width {
+                to: 0
+                duration: Config.notifications.toastDuration
+                paused: toast.containsMouse && timeBar.visible
+                running: timeBar.visible
             }
         }
-        running: !toast.containsMouse
     }
 }
diff --git a/src/windows/notificationtoast/ToastAction.qml b/src/widgets/notifications/NotificationToastAction.qml
similarity index 100%
rename from src/windows/notificationtoast/ToastAction.qml
rename to src/widgets/notifications/NotificationToastAction.qml
diff --git a/src/windows/notificationtoast/NotificationToasts.qml b/src/windows/notificationtoast/NotificationToasts.qml
index 11fb4cd..0651436 100644
--- a/src/windows/notificationtoast/NotificationToasts.qml
+++ b/src/windows/notificationtoast/NotificationToasts.qml
@@ -3,6 +3,7 @@ import Quickshell
 import QtQuick.Controls
 import "root:provider"
 import "root:base"
+import "../../widgets/notifications"
 
 PanelWindow {
     id: popups
@@ -36,11 +37,15 @@ PanelWindow {
             anchors.margins: lbar.width * 0.2
             anchors.fill: parent
             focus: true
+            spacing: 10
+
             model: ListModel {
                 id: data
                 Component.onCompleted: () => {
-                    Notifications._.notification.connect(e => {
-                        data.insert(0, e);
+                    Notifications.incomingAdded.connect(n => {
+                        data.insert(0, {
+                            notif: n
+                        });
                     });
                 }
             }
@@ -80,8 +85,40 @@ PanelWindow {
                 }
             }
 
-            spacing: 10
-            delegate: Toast {}
+            delegate: NotificationToast {
+                id: toast
+
+                property int countdownTime: Config.notifications.toastDuration
+                required property int index
+
+                width: ListView.view.width
+                showTimeBar: true
+
+                Timer {
+                    id: timer
+                    interval: 100
+                    onTriggered: () => {
+                        toast.countdownTime -= interval;
+                        if (toast.countdownTime <= 0) {
+                            toast.close();
+                        }
+                    }
+                    repeat: true
+                    running: !toast.containsMouse && toast.countdownTime > 0
+                }
+
+                Component.onCompleted: notif.closed.connect(() => {
+                    if (!toast || toast.index < 0)
+                        return;
+                    ListView.view.model.remove(toast.index, 1);
+                })
+
+                onClose: {
+                    if (!toast || toast.index < 0)
+                        return;
+                    ListView.view.model.remove(toast.index, 1);
+                }
+            }
         }
     }
 }