From 3b2628fbd7220c32cfe93955d202518fd584ab61 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 19 Apr 2025 20:07:44 +0200 Subject: [PATCH] notifications properly working --- .../common/widgets/NotificationWidget.qml | 260 +++++++++++------- .../common/widgets/notification_utils.js | 4 +- .../notifications/NotificationList.qml | 58 +++- .../modules/sidebarRight/todo/TaskList.qml | 1 - .config/quickshell/services/Notifications.qml | 82 +++++- .config/quickshell/services/Todo.qml | 2 +- 6 files changed, 288 insertions(+), 119 deletions(-) diff --git a/.config/quickshell/modules/common/widgets/NotificationWidget.qml b/.config/quickshell/modules/common/widgets/NotificationWidget.qml index 096125feb..160501410 100644 --- a/.config/quickshell/modules/common/widgets/NotificationWidget.qml +++ b/.config/quickshell/modules/common/widgets/NotificationWidget.qml @@ -8,129 +8,201 @@ import Quickshell.Widgets import Quickshell.Services.Notifications import "./notification_utils.js" as NotificationUtils -WrapperRectangle { +Item { id: root property var notificationObject - property bool expanded: true + property bool expanded: false + property bool enableAnimation: true + property int notificationListSpacing: 5 + property bool ready: false Layout.fillWidth: true - color: (notificationObject.urgency == NotificationUrgency.Critical) ? - Appearance.m3colors.m3secondaryContainer : Appearance.colors.colLayer2 - radius: Appearance.rounding.normal - RowLayout { + clip: true + + // implicitHeight: notificationRowLayout.implicitHeight + implicitHeight: ready ? notificationRowLayout.implicitHeight + notificationListSpacing : 0 + Behavior on implicitHeight { + enabled: enableAnimation + NumberAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + } + + Component.onCompleted: { + root.ready = true + } + + function fancyDestroy() { + implicitHeight = 0 + notificationRowWrapper.anchors.top = undefined + notificationRowWrapper.anchors.bottom = root.bottom + destroyTimer.start() + } + + Timer { + id: destroyTimer + interval: Appearance.animation.elementDecel.duration + repeat: false + onTriggered: { + root.destroy() + } + } + + MouseArea { // Middle click to close anchors.fill: parent - Rectangle { - id: iconRectangle - implicitWidth: 47 - implicitHeight: 47 - Layout.leftMargin: 10 - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.alignment: Qt.AlignTop - radius: Appearance.rounding.full - color: (notificationObject.urgency == NotificationUrgency.Critical) ? - Appearance.m3colors.m3secondary : Appearance.m3colors.m3secondaryContainer - MaterialSymbol { - visible: notificationObject.appIcon == "" - text: NotificationUtils.guessMessageType(notificationObject.summary) - anchors.fill: parent - color: (notificationObject.urgency == NotificationUrgency.Critical) ? - Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer - font.pixelSize: 27 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - IconImage { - visible: notificationObject.appIcon != "" - anchors.centerIn: parent - implicitSize: 33 - asynchronous: true - source: Quickshell.iconPath(notificationObject.appIcon) + acceptedButtons: Qt.MiddleButton + onClicked: (mouse) => { + if (mouse.button == Qt.MiddleButton) { + Notifications.discardNotification(notificationObject.id) } } - ColumnLayout { - spacing: 0 - RowLayout { - Layout.topMargin: 10 - Layout.leftMargin: 10 - Layout.rightMargin: 10 - Layout.fillWidth: true - StyledText { - Layout.fillWidth: true - horizontalAlignment: Text.AlignLeft - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colOnLayer2 - text: notificationObject.summary - wrapMode: expanded ? Text.Wrap : Text.NoWrap - elide: Text.ElideRight - } - Item { Layout.fillWidth: true } - StyledText { - id: notificationTimeText - Layout.fillWidth: false - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignLeft - font.pixelSize: Appearance.font.pixelSize.small - color: Appearance.m3colors.m3outline - text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) + } - Connections { - target: DateTime - function onTimeChanged() { - notificationTimeText.text = NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) + // Background + Rectangle { + anchors.fill: parent + anchors.topMargin: notificationListSpacing + color: (notificationObject.urgency == NotificationUrgency.Critical) ? + Appearance.m3colors.m3secondaryContainer : Appearance.colors.colLayer2 + radius: Appearance.rounding.normal + } + + + Item { + id: notificationRowWrapper + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + implicitHeight: notificationRowLayout.implicitHeight + notificationListSpacing + RowLayout { + id: notificationRowLayout + + anchors.left: parent.left + anchors.right: parent.right + // anchors.top: parent.top + anchors.bottom: parent.bottom + Rectangle { // App icon + id: iconRectangle + implicitWidth: 47 + implicitHeight: 47 + Layout.leftMargin: 10 + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.alignment: Qt.AlignTop + Layout.fillWidth: false + radius: Appearance.rounding.full + color: (notificationObject.urgency == NotificationUrgency.Critical) ? + Appearance.m3colors.m3secondary : Appearance.m3colors.m3secondaryContainer + MaterialSymbol { + visible: notificationObject.appIcon == "" + text: NotificationUtils.guessMessageType(notificationObject.summary) + anchors.fill: parent + color: (notificationObject.urgency == NotificationUrgency.Critical) ? + Appearance.m3colors.m3onSecondary : Appearance.m3colors.m3onSecondaryContainer + font.pixelSize: 27 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + IconImage { + visible: notificationObject.appIcon != "" + anchors.centerIn: parent + implicitSize: 33 + asynchronous: true + source: Quickshell.iconPath(notificationObject.appIcon) + } + } + ColumnLayout { // Notification content + spacing: 0 + Layout.fillWidth: true + + RowLayout { // Row of summary, time and expand button + Layout.topMargin: 10 + Layout.leftMargin: 10 + Layout.rightMargin: 10 + Layout.fillWidth: true + + StyledText { // Summary + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer2 + text: notificationObject.summary + wrapMode: expanded ? Text.Wrap : Text.NoWrap + elide: Text.ElideRight + } + + Item { Layout.fillWidth: true } + + StyledText { // Time + id: notificationTimeText + Layout.fillWidth: false + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignLeft + font.pixelSize: Appearance.font.pixelSize.smaller + color: Appearance.m3colors.m3outline + text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) + + Connections { + target: DateTime + function onTimeChanged() { + notificationTimeText.text = NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) + } } } - } - Button { - Layout.alignment: Qt.AlignVCenter - id: expandButton - implicitWidth: 22 - implicitHeight: 22 - onClicked: { - root.expanded = !root.expanded - } - PointingHandInteraction{} + Button { // Expand button + Layout.alignment: Qt.AlignVCenter + id: expandButton + implicitWidth: 22 + implicitHeight: 22 - background: Rectangle { - anchors.fill: parent - radius: Appearance.rounding.full - color: (expandButton.down) ? Appearance.colors.colLayer2Active : (expandButton.hovered ? Appearance.colors.colLayer2Hover : Appearance.transparentize(Appearance.colors.colLayer2, 1)) + PointingHandInteraction{} + onClicked: { + root.enableAnimation = true + root.expanded = !root.expanded + } + + background: Rectangle { + anchors.fill: parent + radius: Appearance.rounding.full + color: (expandButton.down) ? Appearance.colors.colLayer2Active : (expandButton.hovered ? Appearance.colors.colLayer2Hover : Appearance.transparentize(Appearance.colors.colLayer2, 1)) + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } - Behavior on color { - ColorAnimation { - duration: Appearance.animation.elementDecel.duration - easing.type: Appearance.animation.elementDecel.type } } - } - contentItem: MaterialSymbol { - anchors.centerIn: parent - text: expanded ? "keyboard_arrow_up" : "keyboard_arrow_down" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Appearance.font.pixelSize.normal - color: Appearance.colors.colOnLayer2 - } + contentItem: MaterialSymbol { + anchors.centerIn: parent + text: expanded ? "keyboard_arrow_up" : "keyboard_arrow_down" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.normal + color: Appearance.colors.colOnLayer2 + } + } } - } - RowLayout { - StyledText { + StyledText { // Notification body Layout.fillWidth: true Layout.bottomMargin: 10 Layout.leftMargin: 10 Layout.rightMargin: 10 + clip: true wrapMode: expanded ? Text.Wrap : Text.NoWrap elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.small horizontalAlignment: Text.AlignLeft color: Appearance.m3colors.m3outline - textFormat: Text.MarkdownText - text: notificationObject.body + // textFormat: Text.MarkdownText + text: notificationObject.body } } } diff --git a/.config/quickshell/modules/common/widgets/notification_utils.js b/.config/quickshell/modules/common/widgets/notification_utils.js index cb223c83d..e7b82cc8e 100644 --- a/.config/quickshell/modules/common/widgets/notification_utils.js +++ b/.config/quickshell/modules/common/widgets/notification_utils.js @@ -42,8 +42,8 @@ function guessMessageType(summary) { // return messageTime.format(userOptions.time.dateFormat); // } -const getFriendlyNotifTimeString = (timeObject) => { - const messageTime = timeObject; +const getFriendlyNotifTimeString = (timestamp) => { + const messageTime = new Date(timestamp); const now = new Date(); const oneMinuteAgo = new Date(now.getTime() - 60000); diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml index e921c9316..1fb3657ce 100644 --- a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml +++ b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml @@ -7,6 +7,50 @@ import QtQuick.Layouts import Quickshell.Widgets Item { + id: root + property Component notifComponent: NotificationWidget {} + property list notificationWidgetList: [] + + // Signal handlers to add/remove notifications + Connections { + target: Notifications + function onInitDone() { + // notificationRepeater.model = Notifications.list.slice().reverse() + Notifications.list.slice().reverse().forEach((notification) => { + const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification }); + notificationWidgetList.push(notif) + }) + } + function onNotify(notification) { + // notificationRepeater.model = [notification, ...notificationRepeater.model] + const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification }); + notificationWidgetList.unshift(notif) + + // Remove stuff from t he column, add back + for (let i = 0; i < notificationWidgetList.length; i++) { + if (notificationWidgetList[i].parent === columnLayout) { + notificationWidgetList[i].parent = null; + } + } + + // Add notification widgets to the column + for (let i = 0; i < notificationWidgetList.length; i++) { + if (notificationWidgetList[i].parent === null) { + notificationWidgetList[i].parent = columnLayout; + } + } + } + function onDiscard(id) { + for (let i = notificationWidgetList.length - 1; i >= 0; i--) { + const widget = notificationWidgetList[i]; + if (widget && widget.notificationObject && widget.notificationObject.id === id) { + widget.fancyDestroy(); + notificationWidgetList.splice(i, 1); + } + } + } + } + Flickable { // Scrollable window id: flickable anchors.fill: parent @@ -14,20 +58,12 @@ Item { clip: true ColumnLayout { // Scrollable window content + id: columnLayout anchors.left: parent.left anchors.right: parent.right - id: columnLayout - - Repeater { - model: Notifications.list - - delegate: NotificationWidget { - notificationObject: modelData - } - - } + spacing: 0 // The widgets themselves have margins for spacing + // Notifications are added by the above signal handlers } - } } \ No newline at end of file diff --git a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml index 89f92dee5..2b920d432 100644 --- a/.config/quickshell/modules/sidebarRight/todo/TaskList.qml +++ b/.config/quickshell/modules/sidebarRight/todo/TaskList.qml @@ -94,7 +94,6 @@ Item { Item { Layout.fillWidth: true } - // layoutDirection: Qt.RightToLeft TodoItemActionButton { Layout.fillWidth: false onClicked: { diff --git a/.config/quickshell/services/Notifications.qml b/.config/quickshell/services/Notifications.qml index db2eadc5a..3d0cf1dd8 100644 --- a/.config/quickshell/services/Notifications.qml +++ b/.config/quickshell/services/Notifications.qml @@ -3,30 +3,92 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Services.Notifications +import Qt.labs.platform Singleton { id: root - property alias list: notifServer.trackedNotifications + property var filePath: `${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/notifications/notifications.json` + property var list: [] + + signal initDone(); + signal notify(notification: var); + signal discard(id: var); NotificationServer { id: notifServer - actionIconsSupported: true - actionsSupported: true + // actionIconsSupported: true + // actionsSupported: true bodyHyperlinksSupported: true - bodyImagesSupported: true + // bodyImagesSupported: true bodyMarkupSupported: true bodySupported: true - imageSupported: true - keepOnReload: true + // imageSupported: true + keepOnReload: false // I can't figure out RetainableLock, using a custom solution with a json file instead persistenceSupported: true onNotification: (notification) => { - notification.tracked = true; - if(!notification.time) { - notification.time = new Date(); + notification.tracked = true + const newNotifObject = { + "id": notification.id, + "actions": [], + "appIcon": notification.appIcon, + "appName": notification.appName, + "body": notification.body, + "summary": notification.summary, + "time": Date.now(), + "urgency": notification.urgency.toString(), + } + root.list = [...root.list, newNotifObject]; + root.notify(newNotifObject); + notifFileView.setText(JSON.stringify(root.list, null, 2)) + } + } + + function discardNotification(id) { + const index = root.list.findIndex((notif) => notif.id === id); + const notifServerIndex = notifServer.trackedNotifications.values.findIndex((notif) => notif.id === id); + if (index !== -1) { + root.list.splice(index, 1); + notifFileView.setText(JSON.stringify(root.list, null, 2)) + triggerListChange() + } + if (notifServerIndex !== -1) { + notifServer.trackedNotifications.values[notifServerIndex].dismiss() + } + root.discard(id); + } + + function triggerListChange() { + root.list = root.list.slice(0) + } + + function refresh() { + notifFileView.reload() + } + + Component.onCompleted: { + refresh() + } + + FileView { + id: notifFileView + path: filePath + onLoaded: { + const fileContents = notifFileView.text() + root.list = JSON.parse(fileContents) + console.log("[Notifications] File loaded") + root.initDone() + } + onLoadFailed: (error) => { + if(error == FileViewError.FileNotFound) { + console.log("[Notifications] File not found, creating new file.") + root.list = [] + notifFileView.setText(JSON.stringify(root.list)) + } else { + console.log("[Notifications] Error loading file: " + error) } - // root.list = [...root.list, notification]; } } } diff --git a/.config/quickshell/services/Todo.qml b/.config/quickshell/services/Todo.qml index 6267768ce..72f8055d1 100644 --- a/.config/quickshell/services/Todo.qml +++ b/.config/quickshell/services/Todo.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import Quickshell; import Quickshell.Io; -import Quickshell.Services.Pipewire; import Qt.labs.platform import QtQuick; @@ -68,6 +67,7 @@ Singleton { onLoaded: { const fileContents = todoFileView.text() root.list = JSON.parse(fileContents) + console.log("[To Do] File loaded") } onLoadFailed: (error) => { if(error == FileViewError.FileNotFound) {