From e1469df7ff74f9cda48d33d722a87bb93dfb6f9b Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 17 Nov 2025 11:21:07 +0100 Subject: [PATCH 1/2] listview tests, broken --- Daemons/NotifServer.qml | 252 +++++++++++++++++++++++++++++++ Helpers/NotifPath.qml | 16 ++ Helpers/Time.qml | 20 +++ Modules/NotifServer.qml | 45 ------ Modules/NotifServerOld.qml | 52 +++++++ Modules/NotificationCenter.qml | 260 +++++++++++++++++++------------- Modules/TrackedNotification.qml | 2 +- shell.qml | 2 +- 8 files changed, 498 insertions(+), 151 deletions(-) create mode 100644 Daemons/NotifServer.qml create mode 100644 Helpers/NotifPath.qml create mode 100644 Helpers/Time.qml delete mode 100644 Modules/NotifServer.qml create mode 100644 Modules/NotifServerOld.qml diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml new file mode 100644 index 0000000..de9679a --- /dev/null +++ b/Daemons/NotifServer.qml @@ -0,0 +1,252 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications +import QtQuick +import qs.Modules +import qs.Helpers + +Singleton { + id: root + + property list list: [] + readonly property list notClosed: list.filter( n => !n.closed ) + readonly property list popups: list.filter( n => n.popup ) + property alias dnd: props.dnd + property alias server: server + + property bool loaded + + onListChanged: { + if ( loaded ) { + saveTimer.restart(); + } + if ( root.list.length > 0 ) { + HasNotifications.hasNotifications = true; + } else { + HasNotifications.hasNotifications = false; + } + } + + Timer { + id: saveTimer + interval: 1000 + onTriggered: storage.setText( JSON.stringify( root.notClosed.map( n => ({ + time: n.time, + id: n.id, + summary: n.summary, + body: n.body, + appIcon: n.appIcon, + appName: n.appName, + image: n.image, + expireTimeout: n.expireTimeout, + urgency: n.urgency, + resident: n.resident, + hasActionIcons: n.hasActionIcons, + actions: n.actions + })))); + } + + PersistentProperties { + id: props + + property bool dnd + + reloadableId: "notifs" + } + + NotificationServer { + id: server + + keepOnReload: false + actionsSupported: true + bodyHyperlinksSupported: true + bodyImagesSupported: true + bodyMarkupSupported: true + imageSupported: true + persistenceSupported: true + + onNotification: notif => { + notif.tracked = true; + + const comp = notifComp.createObject(root, { + popup: !props.dnd, + notification: notif + }); + root.list = [comp, ...root.list]; + } + } + + FileView { + id: storage + path: NotifPath.notifPath + + onLoaded: { + const data = JSON.parse(text()); + for (const notif of data) + root.list.push(notifComp.createObject(root, notif)); + root.list.sort((a, b) => b.time - a.time); + root.loaded = true; + } + onLoadFailed: err => { + if (err === FileViewError.FileNotFound) { + root.loaded = true; + setText("[]"); + } + } + } + + component Notif: QtObject { + id: notif + + property bool popup + property bool closed + property var locks: new Set() + + property date time: new Date() + readonly property string timeStr: { + const diff = Time.date.getTime() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) + return qsTr("now"); + + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) + return `${d}d`; + if (h > 0) + return `${h}h`; + return `${m}m`; + } + + property Notification notification + property string id + property string summary + property string body + property string appIcon + property string appName + property string image + property real expireTimeout: 5 + property int urgency: NotificationUrgency.Normal + property bool resident + property bool hasActionIcons + property list actions + + readonly property Timer timer: Timer { + running: true + interval: 5000 + onTriggered: { + notif.popup = false; + } + } + + readonly property Connections conn: Connections { + target: notif.notification + + function onClosed(): void { + notif.close(); + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onActionsChanged(): void { + notif.actions = notif.notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + } + + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } + + function close(): void { + closed = true; + if (locks.size === 0 && root.list.includes(this)) { + root.list = root.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } + + Component.onCompleted: { + if (!notification) + return; + + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + expireTimeout = notification.expireTimeout; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + actions = notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + } + + Component { + id: notificationPopup + TrackedNotification { + centerX: NotificationCenter.posX + } + } + + Component { + id: notifComp + + Notif {} + } +} diff --git a/Helpers/NotifPath.qml b/Helpers/NotifPath.qml new file mode 100644 index 0000000..3d4b3cd --- /dev/null +++ b/Helpers/NotifPath.qml @@ -0,0 +1,16 @@ +pragma Singleton + +import Quickshell.Io +import Quickshell + +Singleton { + id: root + + property alias notifPath: storage.notifPath + + JsonAdapter { + id: storage + + property string notifPath: Quickshell.statePath("notifications.json") + } +} diff --git a/Helpers/Time.qml b/Helpers/Time.qml new file mode 100644 index 0000000..c4b3913 --- /dev/null +++ b/Helpers/Time.qml @@ -0,0 +1,20 @@ +pragma Singleton + +import Quickshell + +Singleton { + property alias enabled: clock.enabled + readonly property date date: clock.date + readonly property int hours: clock.hours + readonly property int minutes: clock.minutes + readonly property int seconds: clock.seconds + + function format(fmt: string): string { + return Qt.formatDateTime(clock.date, fmt); + } + + SystemClock { + id: clock + precision: SystemClock.Seconds + } +} diff --git a/Modules/NotifServer.qml b/Modules/NotifServer.qml deleted file mode 100644 index 5babbf2..0000000 --- a/Modules/NotifServer.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import Quickshell.Services.Notifications -import QtQuick -import qs.Modules - -Scope { - id: root - property list notifIds: [] - property list notifications; - NotificationServer { - id: notificationServer - imageSupported: true - actionsSupported: true - persistenceSupported: true - bodyImagesSupported: true - bodySupported: true - onNotification: notification => { - notification.tracked = true; - notification.receivedTime = Date.now(); - root.notifIds.push(notification.id); - const notif = notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb }); - root.notifications.push(notif); - } - } - - Component { - id: notificationComponent - TrackedNotification { - centerX: notificationCenter.posX - notifIndex: root.notifIds - notifList: root.notifications - onNotifDestroy: { - root.notifications.shift(); - root.notifIds.shift(); - } - } - } - - NotificationCenter { - id: notificationCenter - notifications: notificationServer.trackedNotifications.values - } -} diff --git a/Modules/NotifServerOld.qml b/Modules/NotifServerOld.qml new file mode 100644 index 0000000..381fcf8 --- /dev/null +++ b/Modules/NotifServerOld.qml @@ -0,0 +1,52 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import qs.Modules + +Scope { + id: root + property list notifIds: [] + property list notifications; + // NotificationServer { + // id: notificationServer + // imageSupported: true + // actionsSupported: true + // persistenceSupported: true + // bodyImagesSupported: true + // bodySupported: true + // onNotification: notification => { + // notification.tracked = true; + // notification.receivedTime = Date.now(); + // root.notifIds.push(notification.id); + // const notif = notificationComponent.createObject(root, { notif: notification, visible: !notificationCenter.doNotDisturb }); + // root.notifications.push(notif); + // } + // } + + Connections { + target: NotifServer.server + function onNotification() { + notificationComponent.createObject( root, { notif: NotifServer.list[0] }); + } + } + + Component { + id: notificationComponent + TrackedNotification { + centerX: notificationCenter.posX + notifIndex: root.notifIds + notifList: root.notifications + onNotifDestroy: { + root.notifications.shift(); + root.notifIds.shift(); + } + } + } + + NotificationCenter { + id: notificationCenter + notifications: notificationServer.trackedNotifications.values + } +} diff --git a/Modules/NotificationCenter.qml b/Modules/NotificationCenter.qml index 328a52a..0398ca3 100644 --- a/Modules/NotificationCenter.qml +++ b/Modules/NotificationCenter.qml @@ -9,6 +9,7 @@ import QtQuick import Quickshell.Services.Notifications import qs.Config import qs.Helpers +import qs.Daemons PanelWindow { id: root @@ -19,7 +20,6 @@ PanelWindow { left: true bottom: true } - required property list notifications property bool centerShown: false property alias posX: backgroundRect.x property alias doNotDisturb: dndSwitch.checked @@ -27,14 +27,6 @@ PanelWindow { mask: Region { item: backgroundRect } - onNotificationsChanged: { - if ( root.notifications.length > 0 ) { - HasNotifications.hasNotifications = true; - } else { - HasNotifications.hasNotifications = false; - } - } - IpcHandler { id: ipcHandler target: "root" @@ -88,38 +80,6 @@ PanelWindow { 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(); - } - } - HyprlandFocusGrab { id: focusGrab active: false @@ -180,9 +140,8 @@ PanelWindow { anchors.fill: parent hoverEnabled: true onClicked: { - for ( var app in groupedData.groups ) { - groupedData.groups[ app ].forEach( function( n ) { n.dismiss(); }); - } + for ( const n of NotifServer.notClosed ) + n.close(); } } } @@ -198,6 +157,7 @@ PanelWindow { Flickable { Layout.fillWidth: true Layout.fillHeight: true + pixelAligned: true contentHeight: notificationColumn.implicitHeight clip: true @@ -223,52 +183,68 @@ PanelWindow { } Repeater { - model: Object.keys( groupedData.groups ) + model: ScriptModel { + values: { + const map = new Map(); + for ( const n of NotifServer.notClosed ) + map.set( n.appName, null ); + for ( const n of NotifServer.list ) + map.set( n.appName, null ); + return [ ...map.keys() ]; + } + onValuesChanged: { + root.flagChanged(); + } + } Column { id: groupColumn required property string modelData - property var notifications: groupedData.groups[ modelData ] + property list notifications: NotifServer.list.filter( n => n.appName === modelData ) width: parent.width spacing: 10 property bool shouldShow: false property bool isExpanded: false + function closeAll(): void { + for ( const n of NotifServer.notClosed.filter( n => n.appName === modelData )) + n.close(); + } + Behavior on height { Anim {} } - add: Transition { - id: addTrans - SequentialAnimation { - PauseAnimation { - duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50 - } - ParallelAnimation { - NumberAnimation { - properties: "y"; - from: addTrans.ViewTransition.destination.y - (height / 2); - to: addTrans.ViewTransition.destination.y; - duration: 100; - easing.type: Easing.OutCubic - } - NumberAnimation { - properties: "opacity"; - from: 0; - to: 1; - duration: 100; - easing.type: Easing.OutCubic - } - } - } - } - + // add: Transition { + // id: addTrans + // SequentialAnimation { + // PauseAnimation { + // duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50 + // } + // ParallelAnimation { + // NumberAnimation { + // properties: "y"; + // from: addTrans.ViewTransition.destination.y - (height / 2); + // to: addTrans.ViewTransition.destination.y; + // duration: 100; + // easing.type: Easing.OutCubic + // } + // NumberAnimation { + // properties: "opacity"; + // from: 0; + // to: 1; + // duration: 100; + // easing.type: Easing.OutCubic + // } + // } + // } + // } + // move: Transition { id: moveTrans NumberAnimation { - properties: "opacity"; + properties: "y"; duration: 100; - to: 0; easing.type: Easing.OutCubic } } @@ -311,40 +287,76 @@ PanelWindow { } } - Repeater { - model: groupColumn.notifications - Rectangle { + ListView { + id: groupListView + model: ScriptModel { + id: groupModel + values: groupColumn.isExpanded ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 ) + } + + width: parent.width + spacing: 10 + height: contentHeight + contentHeight: childrenRect.height + clip: false + + pixelAligned: true + boundsBehavior: Flickable.StopAtBounds + displayMarginBeginning: 0 + displayMarginEnd: 5000 + + Behavior on height { + Anim { + duration: 20; + } + } + + add: Transition { + id: add + NumberAnimation { + properties: "y,opacity"; + duration: 100 * ( add.ViewTransition.targetIndexes.length / ( add.ViewTransition.targetIndexes.length < 3 ? 1 : 3 )); + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } + + remove: Transition { + NumberAnimation { + properties: "opacity"; + from: 1; + to: 0; + duration: 300; + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } + + displaced: Transition { + NumberAnimation { + properties: "y"; + duration: 200; + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } + + delegate: Rectangle { id: groupHeader - required property var modelData - required property var index - width: parent.width + required property int index + required property NotifServer.Notif modelData + property alias notifHeight: groupHeader.height + + property bool previewHidden: !groupColumn.isExpanded && index > 0 + + width: groupListView.width height: contentColumn.height + 20 color: Config.baseBgColor border.color: "#555555" border.width: 1 radius: 8 - visible: groupColumn.notifications[0].id === modelData.id || groupColumn.isExpanded - opacity: groupColumn.notifications[0].id === modelData.id ? 1 : 0 + opacity: 1 + scale: 1.0 - Connections { - target: groupColumn - function onShouldShowChanged() { - if ( !shouldShow ) { - // collapseAnim.start(); - } - } - } - - onVisibleChanged: { - if ( visible ) { - // expandAnim.start(); - } else { - if ( groupColumn.notifications[0].id !== modelData.id ) { - groupHeader.opacity = 0; - } - groupColumn.isExpanded = false; - } - } + Component.onCompleted: modelData.lock(this); + Component.onDestruction: modelData.unlock(this); MouseArea { anchors.fill: parent @@ -359,6 +371,44 @@ PanelWindow { } } + ParallelAnimation { + running: groupHeader.modelData.closed + onFinished: groupHeader.modelData.unlock(groupHeader) + + Anim { + target: groupHeader + property: "opacity" + to: 0 + } + Anim { + target: groupHeader + property: "x" + to: groupHeader.x >= 0 ? groupHeader.width : -groupHeader.width + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + Behavior on x { + Anim { + duration: MaterialEasing.expressiveDefaultSpatialTime + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } + + Behavior on y { + Anim { + duration: MaterialEasing.expressiveDefaultSpatialTime + easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial + } + } + Column { id: contentColumn anchors.centerIn: parent @@ -420,13 +470,15 @@ PanelWindow { RowLayout { spacing: 2 - visible: groupColumn.isExpanded ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : ( groupColumn.notifications.length === 1 ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : false ) + // visible: groupColumn.isExpanded ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : ( groupColumn.notifications.length === 1 ? ( groupHeader.modelData.actions.length > 1 ? true : false ) : false ) + visible: true height: 30 width: parent.width Repeater { model: groupHeader.modelData.actions Rectangle { + id: actionButton Layout.fillWidth: true Layout.preferredHeight: 30 required property var modelData @@ -434,7 +486,7 @@ PanelWindow { radius: 4 Text { anchors.centerIn: parent - text: modelData.text + text: actionButton.modelData.text color: "white" font.pointSize: 12 } @@ -443,7 +495,7 @@ PanelWindow { anchors.fill: parent hoverEnabled: true onClicked: { - modelData.invoke(); + actionButton.modelData.invoke(); } } } @@ -472,7 +524,7 @@ PanelWindow { anchors.fill: parent hoverEnabled: true onClicked: { - groupColumn.isExpanded ? groupColumn.notifications[0].dismiss() : groupColumn.notifications.forEach( function( n ) { n.dismiss(); }); + groupColumn.isExpanded ? groupHeader.modelData.close() : groupColumn.closeAll(); } } } diff --git a/Modules/TrackedNotification.qml b/Modules/TrackedNotification.qml index 6dee817..64a6b81 100644 --- a/Modules/TrackedNotification.qml +++ b/Modules/TrackedNotification.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts import QtQuick import Quickshell.Services.Notifications import qs.Config +import qs.Daemons PanelWindow { id: root @@ -21,7 +22,6 @@ PanelWindow { required property Notification notif required property int centerX required property list notifIndex - required property list notifList property int index: notifIndex.indexOf(notif.id) property alias y: backgroundRect.y property alias notifHeight: backgroundRect.implicitHeight diff --git a/shell.qml b/shell.qml index d9fae93..6db3a87 100644 --- a/shell.qml +++ b/shell.qml @@ -6,6 +6,6 @@ import qs.Modules Scope { Bar {} Wallpaper {} - NotifServer {} + NotificationCenter {} Launcher {} } -- 2.47.3 From d8199f792a4b4377cde187ac1c7010d50a75db6e Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Mon, 17 Nov 2025 15:00:22 +0100 Subject: [PATCH 2/2] **animations** oops --- Bar.qml | 4 + Daemons/NotifServer.qml | 7 - Helpers/NotifCenterSpacing.qml | 16 ++ Modules/NotificationCenter.qml | 178 ++++++++++---------- Modules/TrackedNotification.qml | 287 ++++++++++++++++++-------------- shell.qml | 2 +- 6 files changed, 275 insertions(+), 219 deletions(-) create mode 100644 Helpers/NotifCenterSpacing.qml diff --git a/Bar.qml b/Bar.qml index c5b7c7a..5b7ed3b 100644 --- a/Bar.qml +++ b/Bar.qml @@ -17,6 +17,10 @@ Scope { screen: modelData property var root: Quickshell.shellDir + NotificationCenter { + bar: bar + } + Process { id: ncProcess command: ["sh", "-c", `qs -p ${bar.root}/shell.qml ipc call root showCenter`] diff --git a/Daemons/NotifServer.qml b/Daemons/NotifServer.qml index de9679a..2c9b419 100644 --- a/Daemons/NotifServer.qml +++ b/Daemons/NotifServer.qml @@ -237,13 +237,6 @@ Singleton { } } - Component { - id: notificationPopup - TrackedNotification { - centerX: NotificationCenter.posX - } - } - Component { id: notifComp diff --git a/Helpers/NotifCenterSpacing.qml b/Helpers/NotifCenterSpacing.qml new file mode 100644 index 0000000..e5b34ed --- /dev/null +++ b/Helpers/NotifCenterSpacing.qml @@ -0,0 +1,16 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property alias centerX: notifCenterSpacing.centerX + + JsonAdapter { + id: notifCenterSpacing + + property int centerX + } +} diff --git a/Modules/NotificationCenter.qml b/Modules/NotificationCenter.qml index 0398ca3..81d1bc9 100644 --- a/Modules/NotificationCenter.qml +++ b/Modules/NotificationCenter.qml @@ -2,6 +2,7 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Widgets import Quickshell.Io +import Quickshell.Wayland import QtQuick.Layouts import QtQuick.Controls.FluentWinUI3 import QtQuick.Effects @@ -20,9 +21,11 @@ PanelWindow { left: true bottom: true } + + WlrLayershell.layer: WlrLayer.Overlay + required property PanelWindow bar property bool centerShown: false property alias posX: backgroundRect.x - property alias doNotDisturb: dndSwitch.checked visible: false mask: Region { item: backgroundRect } @@ -61,10 +64,10 @@ PanelWindow { id: showAnimation target: backgroundRect property: "x" - from: Screen.width - to: Screen.width - backgroundRect.implicitWidth - 10 - duration: 300 - easing.type: Easing.OutCubic + to: root.bar.screen.width - backgroundRect.implicitWidth - 10 + from: root.bar.screen.width + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects onStopped: { focusGrab.active = true; } @@ -74,10 +77,10 @@ PanelWindow { id: closeAnimation target: backgroundRect property: "x" - from: Screen.width - backgroundRect.implicitWidth - 10 - to: Screen.width - duration: 300 - easing.type: Easing.OutCubic + from: root.bar.screen.width - backgroundRect.implicitWidth - 10 + to: root.bar.screen.width + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects } HyprlandFocusGrab { @@ -89,10 +92,16 @@ PanelWindow { } } + TrackedNotification { + centerShown: root.centerShown + bar: root.bar + } + Rectangle { id: backgroundRect y: 10 x: Screen.width + z: 1 implicitWidth: 400 implicitHeight: root.height - 20 color: Config.baseBgColor @@ -114,6 +123,9 @@ PanelWindow { focus: false activeFocusOnTab: false focusPolicy: Qt.NoFocus + onCheckedChanged: { + NotifServer.dnd = dndSwitch.checked; + } } RowLayout { Layout.alignment: Qt.AlignRight | Qt.AlignVCenter @@ -176,7 +188,7 @@ PanelWindow { move: Transition { NumberAnimation { - properties: "y,x"; + properties: "x"; duration: 200; easing.type: Easing.OutCubic } @@ -215,31 +227,45 @@ PanelWindow { Anim {} } - // add: Transition { - // id: addTrans - // SequentialAnimation { - // PauseAnimation { - // duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50 - // } - // ParallelAnimation { - // NumberAnimation { - // properties: "y"; - // from: addTrans.ViewTransition.destination.y - (height / 2); - // to: addTrans.ViewTransition.destination.y; - // duration: 100; - // easing.type: Easing.OutCubic - // } - // NumberAnimation { - // properties: "opacity"; - // from: 0; - // to: 1; - // duration: 100; - // easing.type: Easing.OutCubic - // } - // } - // } - // } - // + Behavior on y { + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + + add: Transition { + id: addTrans + SequentialAnimation { + PauseAnimation { + duration: ( addTrans.ViewTransition.index - addTrans.ViewTransition.targetIndexes[ 0 ]) * 50 + } + ParallelAnimation { + NumberAnimation { + properties: "y"; + from: addTrans.ViewTransition.destination.y - (height / 2); + to: addTrans.ViewTransition.destination.y; + duration: 100; + easing.type: Easing.OutCubic + } + NumberAnimation { + properties: "opacity"; + from: 0; + to: 1; + duration: 100; + easing.type: Easing.OutCubic + } + NumberAnimation { + properties: "scale"; + from: 0.7; + to: 1.0; + duration: 100 + easing.type: Easing.InOutQuad + } + } + } + } + move: Transition { id: moveTrans NumberAnimation { @@ -281,79 +307,35 @@ PanelWindow { anchors.fill: parent hoverEnabled: true onClicked: { - groupColumn.isExpanded = false; + groupColumn.shouldShow = false; } } } } - ListView { + Repeater { id: groupListView model: ScriptModel { id: groupModel values: groupColumn.isExpanded ? groupColumn.notifications : groupColumn.notifications.slice( 0, 1 ) } - width: parent.width - spacing: 10 - height: contentHeight - contentHeight: childrenRect.height - clip: false - - pixelAligned: true - boundsBehavior: Flickable.StopAtBounds - displayMarginBeginning: 0 - displayMarginEnd: 5000 - - Behavior on height { - Anim { - duration: 20; - } - } - - add: Transition { - id: add - NumberAnimation { - properties: "y,opacity"; - duration: 100 * ( add.ViewTransition.targetIndexes.length / ( add.ViewTransition.targetIndexes.length < 3 ? 1 : 3 )); - easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial - } - } - - remove: Transition { - NumberAnimation { - properties: "opacity"; - from: 1; - to: 0; - duration: 300; - easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial - } - } - - displaced: Transition { - NumberAnimation { - properties: "y"; - duration: 200; - easing.bezierCurve: MaterialEasing.expressiveDefaultSpatial - } - } - - delegate: Rectangle { + Rectangle { id: groupHeader required property int index required property NotifServer.Notif modelData property alias notifHeight: groupHeader.height - property bool previewHidden: !groupColumn.isExpanded && index > 0 + property bool previewHidden: groupColumn.shouldShow && index > 0 - width: groupListView.width + width: parent.width height: contentColumn.height + 20 color: Config.baseBgColor border.color: "#555555" border.width: 1 radius: 8 - opacity: 1 - scale: 1.0 + opacity: previewHidden ? 0 : 1.0 + scale: previewHidden ? 0.7 : 1.0 Component.onCompleted: modelData.lock(this); Component.onDestruction: modelData.unlock(this); @@ -366,11 +348,35 @@ PanelWindow { groupHeader.modelData.actions[0].invoke(); } } else { + groupColumn.shouldShow = true; groupColumn.isExpanded = true; } } } + ParallelAnimation { + id: collapseAnim + running: !groupColumn.shouldShow && index > 0 + + Anim { + target: groupHeader + property: "opacity" + duration: 100 + from: 1 + to: index > 0 ? 0 : 1.0 + } + Anim { + target: groupHeader + property: "scale" + duration: 100 + from: 1 + to: index > 0 ? 0.7 : 1.0 + } + onFinished: { + groupColumn.isExpanded = false; + } + } + ParallelAnimation { running: groupHeader.modelData.closed onFinished: groupHeader.modelData.unlock(groupHeader) diff --git a/Modules/TrackedNotification.qml b/Modules/TrackedNotification.qml index 64a6b81..86421d0 100644 --- a/Modules/TrackedNotification.qml +++ b/Modules/TrackedNotification.qml @@ -1,167 +1,204 @@ import Quickshell import Quickshell.Widgets import Quickshell.Wayland +import Quickshell.Hyprland import QtQuick.Layouts import QtQuick -import Quickshell.Services.Notifications import qs.Config import qs.Daemons +import qs.Helpers PanelWindow { id: root color: "transparent" + screen: root.bar.screen anchors { top: true right: true left: true bottom: true } - mask: Region { item: backgroundRect } + mask: Region { regions: root.notifRegions } 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) - property alias y: backgroundRect.y - property alias notifHeight: backgroundRect.implicitHeight - signal notifDestroy() + property list notifRegions: [] + required property bool centerShown + required property PanelWindow bar + visible: Hyprland.monitorFor(screen).focused Component.onCompleted: { - openAnim.start(); - console.log(root.index); + console.log(NotifServer.list.filter( n => n.popup ).length + " notification popups loaded."); } - Timer { - id: timeout - interval: 5000 - onTriggered: { - closeAnim.start(); + ListView { + id: notifListView + model: ScriptModel { + values: NotifServer.list.filter( n => n.popup ) } - } + anchors.top: parent.top + anchors.bottom: parent.bottom + x: root.centerShown ? root.bar.width - width - 420 : root.bar.width - width - 20 + z: 0 + anchors.topMargin: 54 + width: 400 + spacing: 10 - 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: contentLayout.childrenRect.height + 16 - x: root.centerX - implicitWidth - 20 - y: !root.notifList[ root.index - 1 ] ? 34 + 20 : root.notifList[ root.index - 1 ].y + root.notifList[ root.index - 1 ].notifHeight + 10 - color: Config.baseBgColor - border.color: "#555555" - radius: 8 - - Behavior on y { + Behavior on x { NumberAnimation { - duration: 200 + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + + displaced: Transition { + NumberAnimation { + property: "y" + duration: 100 easing.type: Easing.InOutQuad } } - Column { - id: contentLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: 10 - spacing: 8 - RowLayout { - spacing: 12 - IconImage { - source: root.notif.image - Layout.preferredWidth: 48 - Layout.preferredHeight: 48 - Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft - visible: root.notif.image !== "" + remove: Transition { + id: hideTransition + ParallelAnimation { + NumberAnimation { + property: "opacity" + from: 1 + to: 0 + duration: 200 + easing.type: Easing.InOutQuad } - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.leftMargin: 0 - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - - Text { - text: root.notif.appName - color: "white" - font.bold: true - font.pointSize: 14 - elide: Text.ElideRight - wrapMode: Text.NoWrap - Layout.fillWidth: true - } - - Text { - text: root.notif.summary - color: "white" - font.pointSize: 12 - font.bold: true - elide: Text.ElideRight - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - + NumberAnimation { + property: "x" + to: hideTransition.ViewTransition.destination.x + 200 + duration: 200 + easing.type: Easing.InOutQuad } } - Text { - text: root.notif.body - color: "#dddddd" - font.pointSize: 14 - elide: Text.ElideRight - wrapMode: Text.WordWrap - maximumLineCount: 4 - width: parent.width - } } - 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 + add: Transition { + id: showTransition + ParallelAnimation { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 200 + easing.type: Easing.InOutQuad + } - Text { - anchors.centerIn: parent - text: "✕" - color: closeArea.containsMouse ? "white" : "#888888" - font.pointSize: 12 + NumberAnimation { + property: "x" + from: showTransition.ViewTransition.destination.x + 200 + to: showTransition.ViewTransition.destination.x + duration: 200 + easing.type: Easing.InOutQuad + } + } + } + + component NotifRegion: Region { } + + Component { + id: notifRegion + NotifRegion {} + } + + delegate: Rectangle { + id: backgroundRect + required property NotifServer.Notif modelData + implicitWidth: 400 + implicitHeight: contentLayout.childrenRect.height + 16 + color: Config.baseBgColor + border.color: "#555555" + radius: 8 + + Component.onCompleted: { + root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect })); } - MouseArea { - id: closeArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - root.notif.dismiss(); - root.visible = false; + Column { + id: contentLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 10 + spacing: 8 + RowLayout { + spacing: 12 + IconImage { + source: backgroundRect.modelData.image + Layout.preferredWidth: 48 + Layout.preferredHeight: 48 + Layout.alignment: Qt.AlignHCenter | Qt.AlignLeft + visible: backgroundRect.modelData.image !== "" + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 0 + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + Text { + text: backgroundRect.modelData.appName + color: "white" + font.bold: true + font.pointSize: 14 + elide: Text.ElideRight + wrapMode: Text.NoWrap + Layout.fillWidth: true + } + + Text { + text: backgroundRect.modelData.summary + color: "white" + font.pointSize: 12 + font.bold: true + elide: Text.ElideRight + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + } + } + Text { + text: backgroundRect.modelData.body + color: "#dddddd" + font.pointSize: 14 + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 4 + width: parent.width + } + } + + 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: { + backgroundRect.modelData.close(); + } } } } diff --git a/shell.qml b/shell.qml index 6db3a87..918ada3 100644 --- a/shell.qml +++ b/shell.qml @@ -6,6 +6,6 @@ import qs.Modules Scope { Bar {} Wallpaper {} - NotificationCenter {} + // NotificationCenter {} Launcher {} } -- 2.47.3