From 677fa06b06934174109e00206b2661ed2d686468 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 29 Nov 2025 12:51:31 +0100 Subject: [PATCH] waffles: notifications: image support --- .../common/functions/NotificationUtils.qml | 24 +++ .../common/widgets/NotificationItem.qml | 31 +--- .../ii/modules/waffle/bar/StartButton.qml | 1 + .../NotificationCenterContent.qml | 19 ++- .../WSingleNotification.qml | 157 ++++++++++++++---- 5 files changed, 172 insertions(+), 60 deletions(-) diff --git a/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml index 8a336e8ad..d6adcc0f6 100644 --- a/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml +++ b/dots/.config/quickshell/ii/modules/common/functions/NotificationUtils.qml @@ -84,4 +84,28 @@ Singleton { // Older dates return Qt.formatDateTime(messageTime, "MMMM dd"); } + + function processNotificationBody(body, appName) { + let processedBody = body + + // Clean Chromium-based browsers notifications - remove first line + if (appName) { + const lowerApp = appName.toLowerCase() + const chromiumBrowsers = [ + "brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge" + ] + + if (chromiumBrowsers.some(name => lowerApp.includes(name))) { + const lines = body.split('\n\n') + + if (lines.length > 1 && lines[0].startsWith(' lowerApp.includes(name))) { - const lines = body.split('\n\n') - - if (lines.length > 1 && lines[0].startsWith('") + return NotificationUtils.processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
") } } } ColumnLayout { // Expanded content + id: expandedContentColumn Layout.fillWidth: true opacity: root.expanded ? 1 : 0 visible: opacity > 0 @@ -218,8 +197,8 @@ Item { // Notification item area elide: Text.ElideRight textFormat: Text.RichText text: { - return `` + - `${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` + return `` + + `${NotificationUtils.processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` } onLinkActivated: (link) => { @@ -293,6 +272,8 @@ Item { // Notification item area id: actionRepeater model: notificationObject.actions NotificationActionButton { + id: notifAction + required property var modelData Layout.fillWidth: true buttonText: modelData.text urgency: notificationObject.urgency diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml index f4a15cc00..5ec96eeb4 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml @@ -7,6 +7,7 @@ import qs.services import qs.modules.common import qs.modules.waffle.looks +// TODO: Replace the icon with QMLized svg (with /usr/lib/qt6/bin/svgtoqml) for proper micro-animation AppButton { id: root diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml index 797cee28a..47ad2f92d 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/NotificationCenterContent.qml @@ -20,6 +20,7 @@ WBarAttachedPanelContent { property bool collapsed: false contentItem: ColumnLayout { + id: contentLayout anchors { horizontalCenter: parent.horizontalCenter top: parent.top @@ -41,9 +42,24 @@ WBarAttachedPanelContent { } contentItem: NotificationPaneContent { implicitWidth: calendarColumnLayout.implicitWidth - implicitHeight: Notifications.list.length > 0 ? (notificationArea.height - notificationPane.borderWidth * 2) : 230 + implicitHeight: { + if (Notifications.list.length > 0) { + return ((contentLayout.height - calendarPane.height - contentLayout.spacing) - notificationPane.borderWidth * 2) + } + return 230; + } + Timer { + id: enableTimer + interval: Config.options.hacks.arbitraryRaceConditionDelay + onTriggered: heightBehavior.enabled = true; + } Behavior on implicitHeight { + id: heightBehavior + enabled: false + Component.onCompleted: { + enableTimer.restart(); + } animation: Looks.transition.enter.createObject(this) } } @@ -51,6 +67,7 @@ WBarAttachedPanelContent { } WPane { + id: calendarPane contentItem: ColumnLayout { id: calendarColumnLayout spacing: 0 diff --git a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml index 95780133c..856ddb160 100644 --- a/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml +++ b/dots/.config/quickshell/ii/modules/waffle/notificationCenter/WSingleNotification.qml @@ -1,3 +1,4 @@ +pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell @@ -41,52 +42,115 @@ MouseArea { anchors.margins: contentItem.padding spacing: 19 - RowLayout { + // Header + SingleNotificationHeader { Layout.fillWidth: true + } - ExpandButton { - Layout.topMargin: -2 + // Content + Item { + id: actualContent + Layout.fillWidth: true + Layout.fillHeight: true + property real spacing: 16 + implicitHeight: Math.max(contentColumn.implicitHeight, imageLoader.height) + implicitWidth: contentColumn.implicitWidth + + Loader { + id: imageLoader + active: root.notification.image != "" + sourceComponent: StyledImage { + width: 48 + height: 48 + sourceSize.width: width + sourceSize.height: height + source: root.notification.image + } } - Item { - Layout.fillWidth: true - } + ColumnLayout { + id: contentColumn + anchors { + top: parent.top + left: parent.left + right: parent.right + } + spacing: 3 - NotificationHeaderButton { - Layout.rightMargin: 4 - opacity: root.containsMouse ? 1 : 0 - icon.name: "dismiss" - implicitSize: 12 - onClicked: { - Qt.callLater(() => { - Notifications.discardNotification(root.notification?.notificationId); - }); + SummaryText { + id: summaryText + Layout.leftMargin: imageLoader.active ? imageLoader.width + actualContent.spacing : 0 + } + BodyText { + Layout.leftMargin: imageLoader.active ? imageLoader.width + actualContent.spacing : 0 + // onLineLaidOut: (line) => { + // if (!imageLoader.active) return; + // const dodgeDistance = imageLoader.width + actualContent.spacing; + // // print(line.y, dodgeDistance) + // if (summaryText.height + line.y > dodgeDistance) { + // line.x -= dodgeDistance; + // line.width += dodgeDistance; + // } + // } } } } - ColumnLayout { + // Actions + ActionsRow { Layout.fillWidth: true - spacing: 3 - - SummaryText {} - BodyText {} } - AcrylicButton { - id: groupExpandButton - visible: root.groupExpandControlMessage !== "" + // "+1 notifications" button + GroupExpandButton { Layout.bottomMargin: 2 - horizontalPadding: 10 - implicitHeight: 24 - implicitWidth: expandButtonText.implicitWidth + horizontalPadding * 2 - onClicked: root.groupExpandToggle() - contentItem: Item { - WText { - id: expandButtonText - anchors.centerIn: parent - text: root.groupExpandControlMessage - } + } + } + } + + component SingleNotificationHeader: RowLayout { + ExpandButton { + Layout.topMargin: -2 + } + + Item { + Layout.fillWidth: true + } + + NotificationHeaderButton { + Layout.rightMargin: 4 + opacity: root.containsMouse ? 1 : 0 + icon.name: "dismiss" + implicitSize: 12 + onClicked: { + Qt.callLater(() => { + Notifications.discardNotification(root.notification?.notificationId); + }); + } + } + } + + component ActionsRow: RowLayout { + visible: root.expanded && root.notification.actions.length > 0 + uniformCellSizes: true + Repeater { + id: actionRepeater + model: root.notification.actions + delegate: WBorderedButton { + id: actionButton + Layout.fillHeight: true + required property var modelData + Layout.fillWidth: true + verticalPadding: 16 + horizontalPadding: 12 + text: modelData.text + implicitHeight: actionButtonText.implicitHeight + verticalPadding * 2 + contentItem: WText { + id: actionButtonText + text: actionButton.text + font.pixelSize: Looks.font.pixelSize.large + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap } } } @@ -106,8 +170,17 @@ MouseArea { verticalAlignment: Text.AlignTop wrapMode: Text.Wrap maximumLineCount: root.expanded ? 100 : 1 - text: root.notification?.body + text: { + if (root.expanded) + return `` + `${NotificationUtils.processNotificationBody(root.notification.body, root.notification.appName || root.notification.summary).replace(/\n/g, "
")}`; + return NotificationUtils.processNotificationBody(root.notification.body, root.notification.appName || root.notification.summary).replace(/\n/g, "
"); + } color: Looks.colors.subfg + textFormat: root.expanded ? Text.RichText : Text.StyledText + onLinkActivated: link => { + Qt.openUrlExternally(link); + GlobalStates.sidebarRightOpen = false; + } } component ExpandButton: NotificationHeaderButton { @@ -140,4 +213,20 @@ MouseArea { } } } + + component GroupExpandButton: AcrylicButton { + id: groupExpandButton + visible: root.groupExpandControlMessage !== "" + horizontalPadding: 10 + implicitHeight: 24 + implicitWidth: expandButtonText.implicitWidth + horizontalPadding * 2 + onClicked: root.groupExpandToggle() + contentItem: Item { + WText { + id: expandButtonText + anchors.centerIn: parent + text: root.groupExpandControlMessage + } + } + } }