From add0b3d66f3a4f91949018476addf883f874b386 Mon Sep 17 00:00:00 2001 From: chlorospingus Date: Thu, 22 Jan 2026 13:38:54 -0800 Subject: [PATCH] Add notifications and notification center --- IpcToggle.qml | 2 +- Launcher.qml | 8 ++- NotifCenter.qml | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ Notifs.qml | 152 +++++++++++++++++++++++++++++++++++++++++ shell.qml | 16 +++++ 5 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 NotifCenter.qml create mode 100644 Notifs.qml diff --git a/IpcToggle.qml b/IpcToggle.qml index a989058..531cad9 100644 --- a/IpcToggle.qml +++ b/IpcToggle.qml @@ -10,6 +10,6 @@ IpcHandler { item.open(); } function toggle() { - item.visible ? close() : open() + item.toggle(); } } diff --git a/Launcher.qml b/Launcher.qml index 6a81afe..96a5f80 100644 --- a/Launcher.qml +++ b/Launcher.qml @@ -21,6 +21,9 @@ PanelWindow { visible = true list.visible = true } + function toggle() { + launcher.visible ? close() : open(); + } implicitWidth: 800 exclusiveZone: 0 @@ -63,7 +66,8 @@ PanelWindow { launcher.visible = false break; case Qt.Key_Return: - list.currentItem.execute() + if (list.currentItem) list.currentItem.execute(); + break; } } @@ -148,7 +152,7 @@ PanelWindow { position: 0 } GradientStop { - color: Qt.darker(button.color, 4) + color: Qt.darker(button.color, 3) position: 1 } } diff --git a/NotifCenter.qml b/NotifCenter.qml new file mode 100644 index 0000000..150a13f --- /dev/null +++ b/NotifCenter.qml @@ -0,0 +1,176 @@ +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +PanelWindow { + id: root + anchors { + top: true + right: true + bottom: true + } + visible: false + exclusiveZone: 1 + implicitWidth: 500 + color: "transparent" + + function open() { + root.visible = true; + } + function close() { + exitAnim.start(); + } + function toggle() { + if (exitAnim.running) return; + if (root.visible) close(); + else open(); + } + + required property var tracked + + Rectangle { + color: "#222" + anchors.fill: parent + Text { + id: title + anchors { + left: parent.left + top: parent.top + leftMargin: 12 + topMargin: 12 + } + text: "Notifications" + font.pixelSize: 20 + font.family: "Comfortaa" + font.bold: true + color: "white" + } + Button { + anchors { + top: parent.top + right: parent.right + } + width: clearAllText.width + 32 + height: clearAllText.height + 30 + Text { + id: clearAllText + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + topMargin: 14 + rightMargin: 16 + } + color: "white" + font.pixelSize: 16 + font.family: "Comfortaa" + text: "Clear All" + } + onClicked: {while (root.tracked.values.length) { + root.tracked.values[0].tracked = false; + }} + property var hover: HoverHandler { } + background: Rectangle { + anchors { + fill: parent + topMargin: 6 + leftMargin: 8 + bottomMargin: 6 + rightMargin: 8 + } + radius: 12 + color: parent.hover.hovered ? "#333" : "#222" + Behavior on color { PropertyAnimation { + duration: 150 + }} + } + } + ListView { + anchors { + fill: parent + topMargin: 48 + } + model: root.tracked + spacing: 10 + delegate: Button { + anchors { + left: parent.left + right: parent.right + margins: 12 + } + height: notifContent.childrenRect.height + 32 + Image { + id: notifIcon + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 12 + } + width: 40 + height: 40 + source: Quickshell.iconPath(modelData.appIcon, true) + } + Column { + id: notifContent + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 16 + leftMargin: 16 + (notifIcon.source == "" ? 0 : 44) + rightMargin: 16 + } + spacing: 6 + Text { + anchors { + left: parent.left + right: parent.right + } + color: "white" + font.family: "Comfortaa" + font.pixelSize: 16 + text: modelData.summary + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Text { + anchors { + left: parent.left + right: parent.right + } + color: "white" + font.family: "Comfortaa" + font.pixelSize: 12 + text: modelData.body + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 4 + } + } + background: Rectangle { + color: "#333" + radius: 12 + } + onClicked: modelData.tracked = false + } + } + transform: Translate { + NumberAnimation on x { + running: root.visible + from: root.implicitWidth + to: 0 + duration: 300 + easing.type: Easing.OutQuint + } + NumberAnimation on x { + id: exitAnim + to: root.implicitWidth + duration: 300 + easing.type: Easing.InQuint + onFinished: root.visible = false + } + } + } +} diff --git a/Notifs.qml b/Notifs.qml new file mode 100644 index 0000000..ec7bf11 --- /dev/null +++ b/Notifs.qml @@ -0,0 +1,152 @@ +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Effects + +PanelWindow { + id: root + visible: false + anchors { + top: true + right: true + } + color: "transparent" + aboveWindows: true + implicitWidth: 500 + implicitHeight: 200 + focusable: false + + property var font: { family: "Comfortaa" } + + Component { + id: notifPopup + Rectangle { + id: notifRect + required property var modelData + + y: { + let res = 0; + for (let i = 0; notifsList.children[i] && notifsList.children[i] != this; i++) { + if (!notifsList.children[i].exiting) res += notifsList.children[i].height + 10; + } + return res; + } + Behavior on y { NumberAnimation { + duration: 300 + easing.type: Easing.InOutCubic + }} + property bool exiting: false + width: parent?.width ?? 0 + height: notifContent.childrenRect.height + 32 + radius: 20 + color: "#181818" + + RectangularShadow { + z: -1 + anchors.fill: parent + blur: 12 + radius: parent.radius + } + Image { + id: notifIcon + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 12 + } + width: 40 + height: 40 + source: Quickshell.iconPath(modelData.appIcon, true) + } + Column { + id: notifContent + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 16 + leftMargin: 16 + (notifIcon.source == "" ? 0 : 44) + rightMargin: 16 + } + spacing: 6 + Text { + anchors { + left: parent.left + right: parent.right + } + color: "white" + font.family: "Comfortaa" + font.pixelSize: 16 + text: modelData.summary + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + } + Text { + anchors { + left: parent.left + right: parent.right + } + color: "white" + font.family: "Comfortaa" + font.pixelSize: 12 + text: modelData.body + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 4 + } + } + NumberAnimation on x { + running: root.visible + from: root.width + to: 0 + duration: 300 + easing.type: Easing.OutBack + easing.overshoot: 0.8 + } + NumberAnimation on x { + id: exitAnim + running: false + to: parent.parent.width + duration: 300 + easing.type: Easing.InCubic + onStarted: exiting = true + onFinished: { + if (notifsList.children.length === 1) { + root.visible = false + root.implicitHeight = 200; + } + notifRect.destroy(); + } + } + Timer { + running: true + repeat: false + interval: 5000 + onTriggered: exitAnim.start() + } + Component.onCompleted: { + let sumHeight = 40; + for (const notif of notifsList.children) { + sumHeight += notif.height + 6; + } + root.implicitHeight = Math.max(root.implicitHeight, sumHeight); + } + } + } + + Item { + id: notifsList + anchors { + fill: parent + margins: 12 + leftMargin: 64 + } + } + + function notify(notif) { + if (notif.lastGeneration) return + root.visible = true; + notifPopup.createObject(notifsList, {modelData: notif}); + } +} diff --git a/shell.qml b/shell.qml index 0551021..f49af03 100644 --- a/shell.qml +++ b/shell.qml @@ -1,9 +1,18 @@ import "." as Osu import Quickshell import Quickshell.Io +import Quickshell.Services.Notifications ShellRoot { id: root + + NotificationServer { + id: notificationServer + onNotification: (notif) => { + notif.tracked = true + notifPopup.notify(notif) + } + } readonly property var font: { family: "comfortaa" } @@ -15,4 +24,11 @@ ShellRoot { target: "lock" item: Osu.Lock { } } + Osu.IpcToggle { + target: "notifications" + item: Osu.NotifCenter { + tracked: notificationServer.trackedNotifications + } + } + Osu.Notifs { id: notifPopup } }