From 176b5c79b7c862a0752f57313448b4d65bc90c1d Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sun, 9 Nov 2025 01:13:05 +0100 Subject: [PATCH] notification daemon + center --- Bar.qml | 18 +- Modules/NotifServer.qml | 49 +++ Modules/Notification.qml | 155 +++++++ Modules/NotificationCenter.qml | 449 +++++++++++++++++++ Modules/TrayItem.qml | 2 +- Modules/TrayMenu.qml | 3 + Modules/{SwayNC.qml => oldsway.qml} | 0 notification-test/NotificationCenter.qml | 531 ++++++++++++----------- notification-test/shell.qml | 1 - shell.qml | 2 + 10 files changed, 956 insertions(+), 254 deletions(-) create mode 100644 Modules/NotifServer.qml create mode 100644 Modules/Notification.qml create mode 100644 Modules/NotificationCenter.qml rename Modules/{SwayNC.qml => oldsway.qml} (100%) diff --git a/Bar.qml b/Bar.qml index d0327d7..1cf02eb 100644 --- a/Bar.qml +++ b/Bar.qml @@ -19,6 +19,12 @@ Scope { property bool trayMenuVisible: false screen: modelData + Process { + id: ncProcess + command: ["sh", "-c", "qs -p /home/zach/GitProjects/z-bar-qt/shell.qml ipc call root showCenter"] + running: false + } + anchors { top: true left: true @@ -93,8 +99,18 @@ Scope { Layout.alignment: Qt.AlignVCenter } - SwayNC { + Text { Layout.alignment: Qt.AlignVCenter + text: "\ue7f4" + font.family: "Material Symbols Rounded" + font.pixelSize: 22 + color: "white" + MouseArea { + anchors.fill: parent + onClicked: { + ncProcess.running = true + } + } } } } diff --git a/Modules/NotifServer.qml b/Modules/NotifServer.qml new file mode 100644 index 0000000..5ca060c --- /dev/null +++ b/Modules/NotifServer.qml @@ -0,0 +1,49 @@ +pragma ComponentBehavior: Bound +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import qs.Modules + +Rectangle { + id: root + + Text { + text: "\ue7f4" + font.family: "Material Symbols Rounded" + font.pixelSize: 16 + color: "white" + anchors.centerIn: parent + } + + property list notifIds: [] + NotificationServer { + id: notificationServer + imageSupported: true + actionsSupported: true + persistenceSupported: true + bodyImagesSupported: true + bodySupported: true + onNotification: { + notification.tracked = true; + notification.receivedTime = Date.now(); + root.notifIds.push(notification.id); + notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb }); + } + } + + Component { + id: notificationComponent + Notification { + centerX: notificationCenter.posX + notifIndex: root.notifIds + onNotifDestroy: { + root.notifIds.shift(); + } + } + } + + NotificationCenter { + id: notificationCenter + notifications: notificationServer.trackedNotifications.values + } +} diff --git a/Modules/Notification.qml b/Modules/Notification.qml new file mode 100644 index 0000000..9e749b2 --- /dev/null +++ b/Modules/Notification.qml @@ -0,0 +1,155 @@ +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import QtQuick.Layouts +import QtQuick +import Quickshell.Services.Notifications + +PanelWindow { + id: root + color: "transparent" + anchors { + top: true + right: true + left: true + bottom: true + } + mask: Region { item: backgroundRect } + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + required property Notification notif + required property int centerX + required property list notifIndex + property int index: notifIndex.indexOf(notif.id) + signal notifDestroy() + + Component.onCompleted: { + openAnim.start(); + } + + Timer { + id: timeout + interval: 5000 + onTriggered: { + closeAnim.start(); + } + } + + NumberAnimation { + id: openAnim + target: backgroundRect + property: "x" + from: root.centerX + to: root.centerX - backgroundRect.implicitWidth - 20 + duration: 200 + easing.type: Easing.InOutQuad + onStopped: { timeout.start(); } + } + + NumberAnimation { + id: closeAnim + target: backgroundRect + property: "x" + from: root.centerX - backgroundRect.implicitWidth - 20 + to: root.centerX + duration: 200 + easing.type: Easing.InOutQuad + onStopped: { + root.destroy(); + root.notifDestroy(); + } + } + + Rectangle { + id: backgroundRect + implicitWidth: 400 + implicitHeight: 80 + x: root.centerX - implicitWidth - 20 + y: 34 + 20 + ( root.index * ( implicitHeight + 10 )) + color: "#801a1a1a" + border.color: "#555555" + radius: 8 + + Behavior on y { + NumberAnimation { + duration: 200 + easing.type: Easing.InOutQuad + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + IconImage { + source: root.notif.image + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft + visible: root.notif.image !== "" + } + + ColumnLayout { + Layout.fillWidth: true + Layout.leftMargin: root.notif.image !== "" ? 0 : 16 + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + Text { + text: root.notif.appName + color: "white" + font.bold: true + font.pointSize: 12 + elide: Text.ElideRight + wrapMode: Text.NoWrap + Layout.fillWidth: true + } + + Text { + text: root.notif.summary + color: "white" + font.pointSize: 10 + elide: Text.ElideRight + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Text { + text: root.notif.body + color: "#dddddd" + font.pointSize: 8 + elide: Text.ElideRight + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: 6 + anchors.topMargin: 6 + width: 18 + height: 18 + color: closeArea.containsMouse ? "#FF6077" : "transparent" + radius: 9 + + Text { + anchors.centerIn: parent + text: "✕" + color: closeArea.containsMouse ? "white" : "#888888" + font.pointSize: 12 + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + root.notif.dismiss(); + root.visible = false; + } + } + } + } +} diff --git a/Modules/NotificationCenter.qml b/Modules/NotificationCenter.qml new file mode 100644 index 0000000..ab62ce2 --- /dev/null +++ b/Modules/NotificationCenter.qml @@ -0,0 +1,449 @@ +import Quickshell +import Quickshell.Hyprland +import Quickshell.Widgets +import Quickshell.Io +import QtQuick.Layouts +import QtQuick.Controls.FluentWinUI3 +import QtQuick.Effects +import QtQuick +import Quickshell.Services.Notifications + +PanelWindow { + id: root + color: "transparent" + anchors { + top: true + right: true + left: true + bottom: true + } + required property list notifications + property bool centerShown: false + property alias posX: backgroundRect.x + property alias doNotDisturb: dndSwitch.checked + visible: false + + IpcHandler { + id: ipcHandler + target: "root" + + function showCenter(): void { root.centerShown = !root.centerShown; } + } + + onVisibleChanged: { + if ( root.visible ) { + showAnimation.start(); + } + } + + onCenterShownChanged: { + if ( !root.centerShown ) { + closeAnimation.start(); + closeTimer.start(); + } else { + root.visible = true; + } + } + + Timer { + id: closeTimer + interval: 300 + onTriggered: { + root.visible = false; + } + } + + NumberAnimation { + id: showAnimation + target: backgroundRect + property: "x" + from: Screen.width + to: Screen.width - backgroundRect.implicitWidth - 10 + duration: 300 + easing.type: Easing.OutCubic + } + + NumberAnimation { + id: closeAnimation + target: backgroundRect + property: "x" + from: Screen.width - backgroundRect.implicitWidth - 10 + to: Screen.width + duration: 300 + easing.type: Easing.OutCubic + } + + QtObject { + id: groupedData + property var groups: ({}) + + function updateGroups() { + var newGroups = {}; + for ( var i = 0; i < root.notifications.length; i++ ) { + var notif = root.notifications[ i ]; + var appName = notif.appName || "Unknown"; + if ( !newGroups[ appName ]) { + newGroups[ appName ] = []; + } + newGroups[ appName ].push( notif ); + } + // Sort notifications within each group (latest first) + for ( var app in newGroups ) { + newGroups[ app ].sort(( a, b ) => b.receivedTime - a.receivedTime ); + } + groups = newGroups; + groupsChanged(); + } + + Component.onCompleted: updateGroups() + } + + Connections { + target: root + function onNotificationsChanged() { + groupedData.updateGroups(); + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: { + if ( root.centerShown ) { + root.centerShown = false; + } + } + } + + Rectangle { + id: backgroundRect + y: 10 + x: Screen.width + implicitWidth: 400 + implicitHeight: root.height - 20 + color: "#801a1a1a" + radius: 8 + border.color: "#555555" + border.width: 1 + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 10 + + RowLayout { + Layout.fillWidth: true + Switch { + id: dndSwitch + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + text: "Do Not Disturb" + } + RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + text: "Clear all" + color: "white" + } + Rectangle { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + color: clearArea.containsMouse ? "#15FFFFFF" : "transparent" + radius: 4 + Text { + anchors.centerIn: parent + text: "\ue0b8" + font.family: "Material Symbols Rounded" + font.pointSize: 18 + color: "white" + } + MouseArea { + id: clearArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + for ( var app in groupedData.groups ) { + groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); }); + } + } + } + } + } + } + + Rectangle { + color: "#333333" + Layout.preferredHeight: 1 + Layout.fillWidth: true + } + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: notificationColumn.implicitHeight + clip: true + + Column { + id: notificationColumn + width: parent.width + spacing: 10 + + add: Transition { + NumberAnimation { + properties: "x"; + duration: 300; + easing.type: Easing.OutCubic + } + } + + move: Transition { + NumberAnimation { + properties: "y,x"; + duration: 200; + easing.type: Easing.OutCubic + } + } + + Repeater { + model: Object.keys( groupedData.groups ) + Column { + id: groupColumn + required property string modelData + property var notifications: groupedData.groups[ modelData ] + width: parent.width + spacing: 10 + + property bool shouldShow: false + property bool isExpanded: false + + move: Transition { + NumberAnimation { + properties: "y,x"; + duration: 100; + easing.type: Easing.OutCubic + } + } + + RowLayout { + width: parent.width + height: 30 + + Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.leftMargin: 5 + text: groupColumn.modelData + color: "white" + font.pointSize: 14 + font.bold: true + } + + Rectangle { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.fillHeight: true + Layout.preferredWidth: 30 + color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent" + radius: 4 + visible: groupColumn.isExpanded + Text { + anchors.centerIn: parent + text: "\ue944" + font.family: "Material Symbols Rounded" + font.pointSize: 18 + color: "white" + } + MouseArea { + id: collapseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + groupColumn.isExpanded = false; + } + } + } + } + + Repeater { + model: groupColumn.notifications + Rectangle { + id: groupHeader + required property var modelData + required property var index + width: parent.width + height: groupColumn.isExpanded ? ( modelData.actions.length > 1 ? 130 : 80 ) : 80 + color: "#801a1a1a" + border.color: "#555555" + border.width: 1 + radius: 8 + visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded + + // NumberAnimation { + // id: expandAnim + // target: groupHeader + // property: "y" + // duration: 300 + // easing.type: Easing.OutCubic + // from: (( groupHeader.height / 2 ) * index ) + // to: (( groupHeader.height + 60 ) * index ) + // onStarted: { + // groupColumn.shouldShow = true; + // } + // } + // + // NumberAnimation { + // id: collapseAnim + // target: groupHeader + // property: "y" + // duration: 300 + // easing.type: Easing.OutCubic + // from: (( groupHeader.height + 60 ) * index ) + // to: (( groupHeader.height / 2 ) * index ) + // onStopped: { + // groupColumn.isExpanded = false; + // } + // } + + Connections { + target: groupColumn + function onShouldShowChanged() { + if ( !shouldShow ) { + // collapseAnim.start(); + } + } + } + + onVisibleChanged: { + if ( visible ) { + // expandAnim.start(); + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if ( groupColumn.isExpanded ) { + if ( groupHeader.modelData.actions.length === 1 ) { + groupHeader.modelData.actions[0].invoke(); + } + } else { + groupColumn.isExpanded = true; + } + } + } + + Column { + anchors.fill: parent + anchors.margins: 10 + RowLayout { + height: 80 + width: parent.width + spacing: 10 + + IconImage { + source: groupHeader.modelData.image + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.topMargin: 5 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 4 + + Text { + text: groupHeader.modelData.summary + color: "white" + font.bold: true + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: groupHeader.modelData.body + font.pointSize: 10 + color: "#dddddd" + elide: Text.ElideRight + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + Text { + text: groupColumn.notifications.length > 1 ? ( groupColumn.isExpanded ? "" : "(" + groupColumn.notifications.length + ")" ) : "" + font.pointSize: 10 + color: "#666666" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + } + + RowLayout { + spacing: 2 + visible: groupColumn.isExpanded && groupHeader.modelData.actions.length > 1 + height: 15 + width: parent.width + + Repeater { + model: groupHeader.modelData.actions + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + required property var modelData + color: buttonArea.containsMouse ? "#15FFFFFF" : "#09FFFFFF" + radius: 4 + Text { + anchors.centerIn: parent + text: modelData.text + color: "white" + font.pointSize: 12 + } + MouseArea { + id: buttonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + modelData.invoke(); + } + } + } + } + } + } + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: 6 + anchors.topMargin: 6 + width: 18 + height: 18 + color: closeArea.containsMouse ? "#FF6077" : "transparent" + radius: 9 + + Text { + anchors.centerIn: parent + text: "✕" + color: closeArea.containsMouse ? "white" : "#888888" + font.pointSize: 12 + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); }); + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index b6bb4c0..f750d89 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -59,8 +59,8 @@ MouseArea { if ( mouse.button === Qt.LeftButton ) { root.item.activate(); } else if ( mouse.button === Qt.RightButton ) { - trayMenu.trayMenu = root.item?.menu; trayMenu.visible = !trayMenu.visible; + trayMenu.trayMenu = root.item?.menu; trayMenu.focusGrab = true; } } diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml index 73f4366..81b6804 100644 --- a/Modules/TrayMenu.qml +++ b/Modules/TrayMenu.qml @@ -59,6 +59,9 @@ PanelWindow { if ( visible ) { scaleValue = 0; scaleAnimation.start(); + } else { + root.menuStack.pop(); + backEntry.visible = false; } } diff --git a/Modules/SwayNC.qml b/Modules/oldsway.qml similarity index 100% rename from Modules/SwayNC.qml rename to Modules/oldsway.qml diff --git a/notification-test/NotificationCenter.qml b/notification-test/NotificationCenter.qml index 8f19933..ab62ce2 100644 --- a/notification-test/NotificationCenter.qml +++ b/notification-test/NotificationCenter.qml @@ -1,5 +1,3 @@ -pragma ComponentBehavior: Bound - import Quickshell import Quickshell.Hyprland import Quickshell.Widgets @@ -23,7 +21,6 @@ PanelWindow { property bool centerShown: false property alias posX: backgroundRect.x property alias doNotDisturb: dndSwitch.checked - property alias groupedData: groupedData visible: false IpcHandler { @@ -76,94 +73,35 @@ PanelWindow { easing.type: Easing.OutCubic } - ListModel { + QtObject { id: groupedData - property int totalCount: 0 - property var groupMap: ({}) + property var groups: ({}) - function mapGroups() { - groupedData.groupMap = {}; - for ( var i = 0; i < groupedData.count; i++ ) { - var name = get(i).name; - groupMap[ name ] = i; - } - } - - function updateCount() { - var count = 0; - for ( var i = 0; i < groupedData.count; i++ ) { - count += get(i).notifications.count; - } - totalCount = count; - } - - function ensureGroup(appName) { - for ( var i = 0; i < count; i++ ) { - if ( get(i).name === appName ) { - return get(i).notifications; + function updateGroups() { + var newGroups = {}; + for ( var i = 0; i < root.notifications.length; i++ ) { + var notif = root.notifications[ i ]; + var appName = notif.appName || "Unknown"; + if ( !newGroups[ appName ]) { + newGroups[ appName ] = []; } + newGroups[ appName ].push( notif ); } - var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', root); - append({ name: appName, notifications: model }); - mapGroups(); - return model; - } - - function addNotification(notif) { - var appName = notif.appName - var model = ensureGroup(appName); - model.insert(0, notif); - updateCount(); - } - - function removeNotification(notif) { - var appName = notif.appName || "Unknown"; - var group = get( groupMap[ appName ]); - if ( group.name === appName ) { - root.notifications[ idMap[ notif.id ]].dismiss(); - if ( group.notifications.count === 0 ) { - remove( groupMap[ appName ], 1 ); - mapGroups(); - } + // Sort notifications within each group (latest first) + for ( var app in newGroups ) { + newGroups[ app ].sort(( a, b ) => b.receivedTime - a.receivedTime ); } - updateCount(); + groups = newGroups; + groupsChanged(); } - function removeGroup(notif) { - var appName = notif.appName || "Unknown"; - var group = get(groupMap[ appName ]); - if ( group.name === appName ) { - for ( var i = 0; i < group.notifications.count; i++ ) { - var item = group.notifications.get( i ); - item.dismiss(); - } - remove( groupMap[ appName ], 1 ); - updateCount(); - mapGroups(); - } - } - - function resetGroups(notifications) { - groupedData.clear(); - for (var i = 0; i < root.notifications.length; i++) { - addNotification(root.notifications[i]); - } - updateCount(); - } - - Component.onCompleted: { - resetGroups(root.notifications) - } + Component.onCompleted: updateGroups() } Connections { target: root function onNotificationsChanged() { - if ( root.notifications.length > groupedData.totalCount ) { - groupedData.addNotification( root.notifications[ root.notifications.length - 1 ] ); - groupedData.mapGroups(); - console.log(root.notifications) - } + groupedData.updateGroups(); } } @@ -173,8 +111,6 @@ PanelWindow { onClicked: { if ( root.centerShown ) { root.centerShown = false; - console.log("groups", groupedData.count); - console.log(root.notifications) } } } @@ -189,29 +125,51 @@ PanelWindow { radius: 8 border.color: "#555555" border.width: 1 - clip: true ColumnLayout { - id: mainLayout anchors.fill: parent anchors.margins: 10 spacing: 10 RowLayout { - Layout.preferredHeight: 30 Layout.fillWidth: true - Text { - text: "Notifications" - color: "white" - font.bold: true - font.pointSize: 16 - Layout.fillWidth: true - } - Switch { id: dndSwitch - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true text: "Do Not Disturb" } + RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + text: "Clear all" + color: "white" + } + Rectangle { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + color: clearArea.containsMouse ? "#15FFFFFF" : "transparent" + radius: 4 + Text { + anchors.centerIn: parent + text: "\ue0b8" + font.family: "Material Symbols Rounded" + font.pointSize: 18 + color: "white" + } + MouseArea { + id: clearArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + for ( var app in groupedData.groups ) { + groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); }); + } + } + } + } + } } Rectangle { @@ -220,194 +178,265 @@ PanelWindow { Layout.fillWidth: true } - Column { - id: notificationColumn - Layout.fillHeight: true + Flickable { Layout.fillWidth: true - // width: mainLayout.width - spacing: 10 + Layout.fillHeight: true + contentHeight: notificationColumn.implicitHeight clip: true - move: Transition { - NumberAnimation { - properties: "y,x" - duration: 200 - easing.type: Easing.InOutQuad - } - } - - add: Transition { - NumberAnimation { - properties: "y,x" - duration: 200 - easing.type: Easing.InOutQuad - } - } - - ListView { - id: groupListView - model: groupedData + Column { + id: notificationColumn + width: parent.width spacing: 10 - width: 400 - height: Math.min(contentHeight, notificationColumn.height) - // Layout.fillWidth: true - // Layout.fillHeight: true - delegate: ListView { - required property var modelData - required property int index - property bool isExpanded: false - property bool isExpandedAnim: false - id: listView - visible: true - property ListModel notificationsModel: modelData.notifications - model: notificationsModel - width: notificationColumn.width - implicitHeight: listView.isExpandedAnim ? contentHeight : 80 - clip: true - - spacing: 10 - - onIsExpandedChanged: { - if ( !isExpanded ) { - collapseAnim.start(); - } else { - expandAnim.start(); - } - } + add: Transition { NumberAnimation { - id: collapseAnim - target: listView - property: "implicitHeight" - to: 80 - from: listView.contentHeight - duration: 80 - easing.type: Easing.InOutQuad - onStopped: { - listView.isExpandedAnim = listView.isExpanded; - } + properties: "x"; + duration: 300; + easing.type: Easing.OutCubic } + } + move: Transition { NumberAnimation { - id: expandAnim - target: listView - property: "implicitHeight" - from: 80 - to: listView.contentHeight - duration: 80 - easing.type: Easing.InOutQuad - onStopped: { - listView.isExpandedAnim = listView.isExpanded; - } + properties: "y,x"; + duration: 200; + easing.type: Easing.OutCubic } + } - Behavior on y { - NumberAnimation { - duration: 200 - easing.type: Easing.InOutQuad - } - } + Repeater { + model: Object.keys( groupedData.groups ) + Column { + id: groupColumn + required property string modelData + property var notifications: groupedData.groups[ modelData ] + width: parent.width + spacing: 10 - displaced: Transition { - NumberAnimation { - properties: "y,x" - duration: 200 - easing.type: Easing.InOutQuad - } - } + property bool shouldShow: false + property bool isExpanded: false - delegate: Rectangle { - id: notificationItem - required property var modelData - required property int index - width: listView.width - height: 80 - color: "#801a1a1a" - border.color: "#555555" - border.width: 1 - radius: 8 - clip: true - visible: true - opacity: 1 - - MouseArea { - anchors.fill: parent - onClicked: { - listView.isExpanded ? ( notificationItem.index === 0 ? listView.isExpanded = false : null ) : listView.isExpanded = true; + move: Transition { + NumberAnimation { + properties: "y,x"; + duration: 100; + easing.type: Easing.OutCubic } } RowLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 10 + width: parent.width + height: 30 - IconImage { - source: notificationItem.modelData.image - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Text { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.leftMargin: 5 + text: groupColumn.modelData + color: "white" + font.pointSize: 14 + font.bold: true } - ColumnLayout { - Layout.fillWidth: true + Rectangle { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.fillHeight: true - spacing: 4 - + Layout.preferredWidth: 30 + color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent" + radius: 4 + visible: groupColumn.isExpanded Text { - text: notificationItem.modelData.appName + anchors.centerIn: parent + text: "\ue944" + font.family: "Material Symbols Rounded" + font.pointSize: 18 color: "white" - font.bold: true - font.pointSize: 12 - elide: Text.ElideRight - Layout.fillWidth: true } - - Text { - text: notificationItem.modelData.summary - color: "white" - font.pointSize: 10 - elide: Text.ElideRight - Layout.fillWidth: true - Layout.alignment: Qt.AlignVCenter - } - - Text { - text: notificationItem.modelData.body - color: "#dddddd" - font.pointSize: 8 - elide: Text.ElideRight - wrapMode: Text.WordWrap - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignVCenter + MouseArea { + id: collapseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + groupColumn.isExpanded = false; + } } } } - Rectangle { - anchors.right: parent.right - anchors.top: parent.top - anchors.rightMargin: 6 - anchors.topMargin: 6 - width: 18 - height: 18 - color: closeArea.containsMouse ? "#FF6077" : "transparent" - radius: 9 + Repeater { + model: groupColumn.notifications + Rectangle { + id: groupHeader + required property var modelData + required property var index + width: parent.width + height: groupColumn.isExpanded ? ( modelData.actions.length > 1 ? 130 : 80 ) : 80 + color: "#801a1a1a" + border.color: "#555555" + border.width: 1 + radius: 8 + visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded - Text { - anchors.centerIn: parent - text: "✕" - color: closeArea.containsMouse ? "white" : "#888888" - font.pointSize: 12 - } + // NumberAnimation { + // id: expandAnim + // target: groupHeader + // property: "y" + // duration: 300 + // easing.type: Easing.OutCubic + // from: (( groupHeader.height / 2 ) * index ) + // to: (( groupHeader.height + 60 ) * index ) + // onStarted: { + // groupColumn.shouldShow = true; + // } + // } + // + // NumberAnimation { + // id: collapseAnim + // target: groupHeader + // property: "y" + // duration: 300 + // easing.type: Easing.OutCubic + // from: (( groupHeader.height + 60 ) * index ) + // to: (( groupHeader.height / 2 ) * index ) + // onStopped: { + // groupColumn.isExpanded = false; + // } + // } - MouseArea { - id: closeArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - root.notifications[modelData].dismiss(); + Connections { + target: groupColumn + function onShouldShowChanged() { + if ( !shouldShow ) { + // collapseAnim.start(); + } + } + } + + onVisibleChanged: { + if ( visible ) { + // expandAnim.start(); + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if ( groupColumn.isExpanded ) { + if ( groupHeader.modelData.actions.length === 1 ) { + groupHeader.modelData.actions[0].invoke(); + } + } else { + groupColumn.isExpanded = true; + } + } + } + + Column { + anchors.fill: parent + anchors.margins: 10 + RowLayout { + height: 80 + width: parent.width + spacing: 10 + + IconImage { + source: groupHeader.modelData.image + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.topMargin: 5 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 4 + + Text { + text: groupHeader.modelData.summary + color: "white" + font.bold: true + font.pointSize: 12 + elide: Text.ElideRight + Layout.fillWidth: true + } + + Text { + text: groupHeader.modelData.body + font.pointSize: 10 + color: "#dddddd" + elide: Text.ElideRight + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + Text { + text: groupColumn.notifications.length > 1 ? ( groupColumn.isExpanded ? "" : "(" + groupColumn.notifications.length + ")" ) : "" + font.pointSize: 10 + color: "#666666" + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + } + + RowLayout { + spacing: 2 + visible: groupColumn.isExpanded && groupHeader.modelData.actions.length > 1 + height: 15 + width: parent.width + + Repeater { + model: groupHeader.modelData.actions + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + required property var modelData + color: buttonArea.containsMouse ? "#15FFFFFF" : "#09FFFFFF" + radius: 4 + Text { + anchors.centerIn: parent + text: modelData.text + color: "white" + font.pointSize: 12 + } + MouseArea { + id: buttonArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + modelData.invoke(); + } + } + } + } + } + } + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: 6 + anchors.topMargin: 6 + width: 18 + height: 18 + color: closeArea.containsMouse ? "#FF6077" : "transparent" + radius: 9 + + Text { + anchors.centerIn: parent + text: "✕" + color: closeArea.containsMouse ? "white" : "#888888" + font.pointSize: 12 + } + + MouseArea { + id: closeArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); }); + } + } } } } diff --git a/notification-test/shell.qml b/notification-test/shell.qml index c012fff..372d8bc 100644 --- a/notification-test/shell.qml +++ b/notification-test/shell.qml @@ -17,7 +17,6 @@ Scope { notification.tracked = true; notification.receivedTime = Date.now(); root.notifIds.push(notification.id); - notificationCenter.groupedData.addNotification(notification); notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb }); } } diff --git a/shell.qml b/shell.qml index 6e48a28..e802995 100644 --- a/shell.qml +++ b/shell.qml @@ -1,7 +1,9 @@ //@ pragma UseQApplication import Quickshell +import qs.Modules Scope { Bar {} // Wallpaper {} + NotifServer {} }