diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-18 23:39:23 +1000 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-18 23:39:23 +1000 |
| commit | ef5936d0ab58b79d55d79da0c77627f09676691d (patch) | |
| tree | 04a21d49d00d912b23a1665dc5ac1fc4492c6aee | |
| parent | sidebar: add notifs (diff) | |
| download | caelestia-shell-ef5936d0ab58b79d55d79da0c77627f09676691d.tar.gz caelestia-shell-ef5936d0ab58b79d55d79da0c77627f09676691d.tar.bz2 caelestia-shell-ef5936d0ab58b79d55d79da0c77627f09676691d.zip | |
notifs: persistent notifs + better sidebar notifs
| -rw-r--r-- | modules/lock/NotifGroup.qml | 32 | ||||
| -rw-r--r-- | modules/notifications/Content.qml | 2 | ||||
| -rw-r--r-- | modules/notifications/Notification.qml | 19 | ||||
| -rw-r--r-- | modules/sidebar/Notif.qml | 38 | ||||
| -rw-r--r-- | modules/sidebar/NotifDock.qml | 5 | ||||
| -rw-r--r-- | modules/sidebar/NotifGroup.qml | 37 | ||||
| -rw-r--r-- | modules/sidebar/NotifGroupList.qml | 136 | ||||
| -rw-r--r-- | services/Notifs.qml | 86 |
8 files changed, 260 insertions, 95 deletions
diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 3669e5e..15342d4 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -31,11 +31,6 @@ StyledRect { radius: Appearance.rounding.normal color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - RetainableLock { - object: root.notifs[0]?.notification ?? null - locked: true - } - RowLayout { id: content @@ -229,6 +224,27 @@ StyledRect { to: notif.implicitHeight } } + + ParallelAnimation { + running: notif.modelData.closed + onFinished: notif.modelData.lock(notif) + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "scale" + to: 0.7 + } + Anim { + target: notif.Layout + property: "preferredHeight" + to: 0 + } + } } } @@ -287,10 +303,8 @@ StyledRect { } color: root.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - RetainableLock { - object: notifLine.modelData.notification - locked: true - } + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) TextMetrics { id: metrics diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 828c3a8..019e922 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -55,7 +55,7 @@ Item { id: list model: ScriptModel { - values: [...Notifs.popups].reverse() + values: Notifs.popups.filter(n => !n.closed).reverse() } anchors.fill: parent diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 2d56ef6..95507fc 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -26,7 +26,11 @@ StyledRect { implicitHeight: inner.implicitHeight x: Config.notifs.sizes.width - Component.onCompleted: x = 0 + Component.onCompleted: { + x = 0; + modelData.lock(this); + } + Component.onDestruction: modelData.unlock(this) Behavior on x { Anim { @@ -34,11 +38,6 @@ StyledRect { } } - RetainableLock { - object: root.modelData.notification - locked: true - } - MouseArea { property int startY @@ -61,7 +60,7 @@ StyledRect { root.modelData.timer.stop(); startY = event.y; if (event.button === Qt.MiddleButton) - root.modelData.notification.dismiss(); + root.modelData.close(); } onReleased: event => { if (!containsMouse) @@ -70,7 +69,7 @@ StyledRect { if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) root.x = 0; else - root.modelData.notification.dismiss(); // TODO: change back to popup when notif dock impled + root.modelData.popup = false; } onPositionChanged: event => { if (pressed) { @@ -393,7 +392,7 @@ StyledRect { return; Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.modelData.notification.dismiss(); // TODO: change back to popup when notif dock impled + root.modelData.popup = false; } opacity: root.expanded ? 1 : 0 @@ -422,7 +421,7 @@ StyledRect { modelData: QtObject { readonly property string text: qsTr("Close") function invoke(): void { - root.modelData.notification.dismiss(); + root.modelData.close(); } } } diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 8b96792..3aecc59 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -1,10 +1,8 @@ pragma ComponentBehavior: Bound import qs.components -import qs.components.controls import qs.services import qs.config -import Quickshell import QtQuick import QtQuick.Layouts @@ -15,8 +13,9 @@ StyledRect { required property Props props required property bool expanded - Layout.fillWidth: true - implicitHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summary.implicitHeight + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summary.implicitHeight + + implicitHeight: nonAnimHeight radius: Appearance.rounding.small color: { @@ -44,34 +43,6 @@ StyledRect { } } - ParallelAnimation { - running: true - - Anim { - target: root - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: root - property: "scale" - from: 0.7 - to: 1 - } - // Anim { - // target: root.Layout - // property: "preferredHeight" - // from: 0 - // to: root.implicitHeight - // } - } - - RetainableLock { - object: root.modelData.notification - locked: true - } - StyledText { id: summary @@ -139,7 +110,8 @@ StyledRect { StyledText { Layout.fillWidth: true - text: root.modelData.body || qsTr("No body here! :/") + textFormat: Text.MarkdownText + text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline wrapMode: Text.WordWrap } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 36b6665..490eeb8 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -160,8 +160,9 @@ Item { repeat: true interval: 50 onTriggered: { - Notifs.list[0]?.notification.dismiss(); - if (Notifs.list.length === 0) + if (Notifs.list.length > 0) + Notifs.list[0].close(); + else stop(); } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 154b530..4476bf9 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import qs.components -import qs.components.controls import qs.components.effects import qs.services import qs.config @@ -119,13 +118,15 @@ StyledRect { } ColumnLayout { + id: column + Layout.topMargin: -Appearance.padding.small Layout.bottomMargin: -Appearance.padding.small / 2 Layout.fillWidth: true - spacing: Math.round(Appearance.spacing.small / 2) + spacing: 0 RowLayout { - Layout.bottomMargin: -parent.spacing + Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 Layout.fillWidth: true spacing: Appearance.spacing.smaller @@ -173,7 +174,7 @@ StyledRect { Layout.leftMargin: Appearance.padding.small / 2 animate: true - text: root.notifs.length + text: root.notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface font.pointSize: Appearance.font.size.small } @@ -186,31 +187,17 @@ StyledRect { } } } - } - - Repeater { - id: notifList - model: ScriptModel { - values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum) + Behavior on Layout.bottomMargin { + Anim {} } + } - Layout.fillWidth: true - - Notif { - id: notif - - props: root.props - expanded: root.expanded - } + NotifGroupList { + props: root.props + notifs: root.notifs + expanded: root.expanded } } } - - // Behavior on implicitHeight { - // Anim { - // duration: Appearance.anim.durations.expressiveDefaultSpatial - // easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - // } - // } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml new file mode 100644 index 0000000..7def80f --- /dev/null +++ b/modules/sidebar/NotifGroupList.qml @@ -0,0 +1,136 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Props props + required property list<var> notifs + required property bool expanded + + readonly property int spacing: Math.round(Appearance.spacing.small / 2) + property bool flag + + Layout.fillWidth: true + implicitHeight: { + const item = repeater.itemAt(repeater.count - 1); + return item ? item.y + item.implicitHeight : 0; + } + + Repeater { + id: repeater + + model: ScriptModel { + values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum) + onValuesChanged: root.flagChanged() + } + + MouseArea { + id: notif + + required property int index + required property Notifs.Notif modelData + + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY + + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.modelData.closed) + y += item.nonAnimHeight + root.spacing; + } + return y; + } + + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight + + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + + drag.target: this + drag.axis: Drag.XAxis + + onPressed: event => { + startY = event.y; + if (event.button === Qt.MiddleButton) + modelData.close(); + } + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData.close(); + } + + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) + + ParallelAnimation { + running: true + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0.7 + to: 1 + } + } + + ParallelAnimation { + running: notif.modelData.closed + onFinished: notif.modelData.unlock(notif) + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width + } + } + + Notif { + id: notifInner + + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + } + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + } +} diff --git a/services/Notifs.qml b/services/Notifs.qml index 823840c..b651442 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components.misc import qs.config +import qs.utils import Quickshell import Quickshell.Io import Quickshell.Services.Notifications @@ -15,6 +16,24 @@ Singleton { readonly property list<Notif> popups: list.filter(n => n.popup) property alias dnd: props.dnd + property bool loaded + + onListChanged: { + if (!loaded) + return; + + storage.setText(JSON.stringify(list.filter(n => !n.closed).map(n => ({ + id: n.id, + summary: n.summary, + body: n.body, + appIcon: n.appIcon, + appName: n.appName, + image: n.image, + expireTimeout: n.expireTimeout, + urgency: n.urgency + })))); + } + PersistentProperties { id: props @@ -32,6 +51,7 @@ Singleton { bodyImagesSupported: true bodyMarkupSupported: true imageSupported: true + persistenceSupported: true onNotification: notif => { notif.tracked = true; @@ -43,6 +63,18 @@ Singleton { } } + FileView { + id: storage + + path: `${Paths.state}/notifs.json` + onLoaded: { + const data = JSON.parse(text()); + for (const notif of data) + root.list.push(notifComp.createObject(root, notif)); + root.loaded = true; + } + } + CustomShortcut { name: "clearNotifs" description: "Clear all notifications" @@ -81,7 +113,10 @@ Singleton { id: notif property bool popup - readonly property date time: new Date() + 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); @@ -94,18 +129,20 @@ Singleton { return `${h}h`; } - required property Notification notification - readonly property string summary: notification.summary - readonly property string body: notification.body - readonly property string appIcon: notification.appIcon - readonly property string appName: notification.appName - readonly property string image: notification.image - readonly property int urgency: notification.urgency - readonly property list<NotificationAction> actions: notification.actions + property Notification notification + property string id: notification?.id ?? "" + property string summary: notification?.summary ?? "" + property string body: notification?.body ?? "" + property string appIcon: notification?.appIcon ?? "" + property string appName: notification?.appName ?? "" + property string image: notification?.image ?? "" + property real expireTimeout: notification?.expireTimeout ?? Config.notifs.defaultExpireTimeout + property int urgency: notification?.urgency ?? NotificationUrgency.Normal + readonly property list<NotificationAction> actions: notification?.actions ?? [] readonly property Timer timer: Timer { running: true - interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : Config.notifs.defaultExpireTimeout + interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout onTriggered: { if (Config.notifs.expire) notif.popup = false; @@ -113,14 +150,33 @@ Singleton { } readonly property Connections conn: Connections { - target: notif.notification.Retainable + target: notif.notification + + function onClosed(): void { + notif.close(); + } + } - function onDropped(): void { - root.list.splice(root.list.indexOf(notif), 1); + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + + if (closed && locks.size === 0 && root.list.includes(this)) { + root.list.splice(root.list.indexOf(this), 1); + notification?.dismiss(); + destroy(); } + } - function onAboutToDestroy(): void { - notif.destroy(); + function close(): void { + closed = true; + if (locks.size === 0 && root.list.includes(this)) { + root.list.splice(root.list.indexOf(this), 1); + notification?.dismiss(); + destroy(); } } } |