From b2d3f401c46968dbbe4e03f74cb477b83ae13b8c Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Sun, 10 Aug 2025 22:02:48 +0200 Subject: [PATCH] Add notification, Use font.pointSize --- Bin/test_notifications.sh | 11 ++ Modules/Notification/Notification.qml | 192 ++++++++++++++++++++++++++ Modules/SidePanel/MediaCard.qml | 12 +- Modules/SidePanel/ProfileCard.qml | 8 +- Modules/SidePanel/SidePanel.qml | 2 +- Modules/SidePanel/WeatherCard.qml | 10 +- Services/NotificationService.qml | 139 +++++++++++++++++++ shell.qml | 5 + 8 files changed, 361 insertions(+), 18 deletions(-) create mode 100755 Bin/test_notifications.sh create mode 100644 Modules/Notification/Notification.qml create mode 100644 Services/NotificationService.qml diff --git a/Bin/test_notifications.sh b/Bin/test_notifications.sh new file mode 100755 index 0000000..4101024 --- /dev/null +++ b/Bin/test_notifications.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "Sending 8 test notifications..." + +# Send 8 notifications with numbers +for i in {1..8}; do + notify-send "Notification $i" "This is test notification number $i of 8" + sleep 1 +done + +echo "All notifications sent!" \ No newline at end of file diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml new file mode 100644 index 0000000..6ced941 --- /dev/null +++ b/Modules/Notification/Notification.qml @@ -0,0 +1,192 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Services.Notifications +import qs.Services +import qs.Widgets + +// Simple notification popup - displays multiple notifications +PanelWindow { + id: root + + readonly property real scaling: Scaling.scale(screen) + + color: "transparent" + visible: notificationService.notificationModel.count > 0 + anchors.top: true + anchors.right: true + margins.top: (Style.barHeight + 10) * scaling + margins.right: 10 * scaling + implicitWidth: 360 * scaling + implicitHeight: Math.min(notificationStack.implicitHeight, (notificationService.maxVisible * 120) * scaling) + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Use the notification service + property var notificationService: NotificationService { } + + // Access the notification model from the service + property ListModel notificationModel: notificationService.notificationModel + + // Track notifications being removed for animation + property var removingNotifications: ({}) + + // Connect to animation signal from service + Component.onCompleted: { + notificationService.animateAndRemove.connect(function(notification, index) { + // Find the delegate and trigger its animation + if (notificationStack.children && notificationStack.children[index]) { + let delegate = notificationStack.children[index] + if (delegate && delegate.animateOut) { + delegate.animateOut() + } + } + }) + } + + + + // Main notification container + Column { + id: notificationStack + anchors.top: parent.top + anchors.right: parent.right + spacing: 8 * scaling + width: 360 * scaling + visible: true + + + + // Multiple notifications display + Repeater { + model: notificationModel + delegate: Rectangle { + width: 360 * scaling + height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginMedium * 2 * scaling)) + clip: true + color: Colors.backgroundPrimary + radius: Style.radiusMedium * scaling + border.color: Colors.backgroundTertiary + border.width: Math.min(1, Style.borderThin * scaling) + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + property bool isRemoving: false + + // Scale and fade-in animation + scale: scaleValue + opacity: opacityValue + + // Animate in when the item is created + Component.onCompleted: { + scaleValue = 1.0 + opacityValue = 1.0 + } + + // Animate out when being removed + function animateOut() { + isRemoving = true + scaleValue = 0.8 + opacityValue = 0.0 + } + + // Timer for delayed removal after animation + Timer { + id: removalTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + notificationService.forceRemoveNotification(model.rawNotification) + } + } + + // Check if this notification is being removed + onIsRemovingChanged: { + if (isRemoving) { + // Remove from model after animation completes + removalTimer.start() + } + } + + // Animation behaviors + Behavior on scale { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.OutBack + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + + + Column { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginSmall * scaling + + RowLayout { + spacing: Style.marginSmall * scaling + NText { + text: (model.appName || model.desktopEntry) || "Unknown App" + color: Colors.accentSecondary + font.pointSize: Style.fontSizeSmall + } + Rectangle { + width: 6 * scaling; height: 6 * scaling; radius: 3 * scaling + color: (model.urgency === NotificationUrgency.Critical) ? Colors.error : + (model.urgency === NotificationUrgency.Low) ? Colors.textSecondary : Colors.accentPrimary + Layout.alignment: Qt.AlignVCenter + } + Item { Layout.fillWidth: true } + NText { + text: notificationService.formatTimestamp(model.timestamp) + color: Colors.textSecondary + font.pointSize: Style.fontSizeSmall + } + } + + NText { + text: model.summary || "No summary" + font.pointSize: Style.fontSizeLarge + font.bold: true + color: Colors.textPrimary + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 3 + elide: Text.ElideRight + } + + NText { + text: model.body || "" + font.pointSize: Style.fontSizeSmall + color: Colors.textSecondary + wrapMode: Text.Wrap + width: 300 * scaling + maximumLineCount: 5 + elide: Text.ElideRight + } + } + + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginSmall * scaling + icon: "close" + onClicked: function() { + animateOut() + } + } + } + } + } +} diff --git a/Modules/SidePanel/MediaCard.qml b/Modules/SidePanel/MediaCard.qml index 6e574f9..ecdddbd 100644 --- a/Modules/SidePanel/MediaCard.qml +++ b/Modules/SidePanel/MediaCard.qml @@ -19,12 +19,10 @@ NBox { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Style.marginXL * scaling - spacing: Style.marginSmall * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling - Item { - height: 36 * scaling - } + Item { height: Style.marginLarge * scaling } Text { text: "music_note" @@ -40,8 +38,6 @@ NBox { anchors.horizontalCenter: parent.horizontalCenter } - Item { - height: 36 * scaling - } + Item { height: Style.marginLarge * scaling } } } diff --git a/Modules/SidePanel/ProfileCard.qml b/Modules/SidePanel/ProfileCard.qml index cd75e56..7488e9d 100644 --- a/Modules/SidePanel/ProfileCard.qml +++ b/Modules/SidePanel/ProfileCard.qml @@ -25,8 +25,8 @@ NBox { Item { id: avatarBox - width: 40 * scaling - height: 40 * scaling + width: Style.baseWidgetSize * 1.25 * scaling + height: Style.baseWidgetSize * 1.25 * scaling Image { id: avatarImage @@ -69,11 +69,11 @@ NBox { } NIconButton { icon: "settings" - sizeMultiplier: 0.8 + sizeMultiplier: 0.9 } NIconButton { icon: "power_settings_new" - sizeMultiplier: 0.8 + sizeMultiplier: 0.9 } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 030b47f..9d32486 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -35,7 +35,7 @@ NLoader { readonly property real scaling: Scaling.scale(screen) // Single source of truth for spacing between cards (both axes) - property real cardSpacing: Style.marginLarge * scaling + property real cardSpacing: Style.spacingLarge * scaling // X coordinate from the bar to align this panel under property real anchorX: root.anchorX // Ensure this panel attaches to the intended screen diff --git a/Modules/SidePanel/WeatherCard.qml b/Modules/SidePanel/WeatherCard.qml index a9881fc..2d2e164 100644 --- a/Modules/SidePanel/WeatherCard.qml +++ b/Modules/SidePanel/WeatherCard.qml @@ -18,8 +18,8 @@ NBox { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Style.marginLarge * scaling - spacing: Style.marginSmall * scaling + anchors.margins: Style.marginMedium * scaling + spacing: Style.marginMedium * scaling RowLayout { spacing: Style.marginSmall * scaling @@ -46,9 +46,9 @@ NBox { color: Colors.backgroundTertiary } - RowLayout { - Layout.fillWidth: true - spacing: Style.marginLarge * scaling + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling Repeater { model: 5 delegate: ColumnLayout { diff --git a/Services/NotificationService.qml b/Services/NotificationService.qml new file mode 100644 index 0000000..52b572d --- /dev/null +++ b/Services/NotificationService.qml @@ -0,0 +1,139 @@ +import QtQuick +import qs.Services +import Quickshell.Services.Notifications + + +QtObject { + id: root + + // Notification server instance + property NotificationServer server: NotificationServer { + id: notificationServer + + // Server capabilities + keepOnReload: false + imageSupported: true + actionsSupported: true + actionIconsSupported: true + bodyMarkupSupported: true + bodySupported: true + persistenceSupported: true + inlineReplySupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + + // Signal when notification is received + onNotification: function(notification) { + + // Track the notification + notification.tracked = true + + // Connect to closed signal for cleanup + notification.closed.connect(function() { + root.removeNotification(notification) + }) + + // Add to our model + root.addNotification(notification) + } + } + + // List model to hold notifications + property ListModel notificationModel: ListModel { } + + // Maximum visible notifications + property int maxVisible: 5 + + // Auto-hide timer + property Timer hideTimer: Timer { + interval: 5000 // 5 seconds + repeat: true + running: notificationModel.count > 0 + + onTriggered: { + if (notificationModel.count === 0) { + return + } + + // Always remove the oldest notification (last in the list) + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification && !oldestNotification.transient) { + // Trigger animation signal instead of direct dismiss + animateAndRemove(oldestNotification, notificationModel.count - 1) + } + } + } + + // Function to add notification to model + function addNotification(notification) { + notificationModel.insert(0, { + rawNotification: notification, + summary: notification.summary, + body: notification.body, + appName: notification.appName, + urgency: notification.urgency, + timestamp: new Date() + }) + + // Remove oldest notifications if we exceed maxVisible + while (notificationModel.count > maxVisible) { + let oldestNotification = notificationModel.get(notificationModel.count - 1).rawNotification + if (oldestNotification) { + oldestNotification.dismiss() + } + notificationModel.remove(notificationModel.count - 1) + } + } + + // Signal to trigger animation before removal + signal animateAndRemove(var notification, int index) + + // Function to remove notification from model + function removeNotification(notification) { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + // Emit signal to trigger animation first + animateAndRemove(notification, i) + break + } + } + } + + // Function to actually remove notification after animation + function forceRemoveNotification(notification) { + for (let i = 0; i < notificationModel.count; i++) { + if (notificationModel.get(i).rawNotification === notification) { + notificationModel.remove(i) + break + } + } + } + + // Function to format timestamp + function formatTimestamp(timestamp) { + if (!timestamp) return "" + + const now = new Date() + const diff = now - timestamp + + // Less than 1 minute + if (diff < 60000) { + return "now" + } + // Less than 1 hour + else if (diff < 3600000) { + const minutes = Math.floor(diff / 60000) + return `${minutes}m ago` + } + // Less than 24 hours + else if (diff < 86400000) { + const hours = Math.floor(diff / 3600000) + return `${hours}h ago` + } + // More than 24 hours + else { + const days = Math.floor(diff / 86400000) + return `${days}d ago` + } + } +} \ No newline at end of file diff --git a/shell.qml b/shell.qml index 8d4748b..66ae026 100644 --- a/shell.qml +++ b/shell.qml @@ -9,6 +9,7 @@ import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background import qs.Modules.SidePanel +import qs.Modules.Notification import qs.Services ShellRoot { @@ -26,4 +27,8 @@ ShellRoot { SidePanel { id: sidePanel } + + Notification { + id: notification + } }