summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-20 15:47:44 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-20 15:47:44 +1000
commit89d46c1b2fd1a35a3229511f745205ac4956bf9e (patch)
treeaa5c7faed9c627b6933cdc533b540eb695af9dd9
parentsidebar/notifs: sort groups by latest notif (diff)
downloadcaelestia-shell-89d46c1b2fd1a35a3229511f745205ac4956bf9e.tar.gz
caelestia-shell-89d46c1b2fd1a35a3229511f745205ac4956bf9e.tar.bz2
caelestia-shell-89d46c1b2fd1a35a3229511f745205ac4956bf9e.zip
sidebar/notifs: better anims + misc fixes
-rw-r--r--modules/lock/NotifDock.qml5
-rw-r--r--modules/lock/NotifGroup.qml4
-rw-r--r--modules/notifications/Content.qml2
-rw-r--r--modules/sidebar/NotifDock.qml134
-rw-r--r--modules/sidebar/NotifDockList.qml150
-rw-r--r--modules/sidebar/NotifGroup.qml19
-rw-r--r--modules/sidebar/NotifGroupList.qml22
-rw-r--r--services/Notifs.qml113
8 files changed, 298 insertions, 151 deletions
diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml
index ac40319..7551e68 100644
--- a/modules/lock/NotifDock.qml
+++ b/modules/lock/NotifDock.qml
@@ -84,7 +84,10 @@ ColumnLayout {
clip: true
model: ScriptModel {
- values: [...new Set(Notifs.list.map(notif => notif.appName))].reverse()
+ values: {
+ const list = Notifs.notClosed.map(n => [n.appName, null]);
+ return [...new Map(list).keys()];
+ }
}
delegate: NotifGroup {}
diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml
index 15342d4..2a08c26 100644
--- a/modules/lock/NotifGroup.qml
+++ b/modules/lock/NotifGroup.qml
@@ -16,7 +16,7 @@ StyledRect {
required property string modelData
- readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData).reverse()
+ readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData)
readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low"
@@ -227,7 +227,7 @@ StyledRect {
ParallelAnimation {
running: notif.modelData.closed
- onFinished: notif.modelData.lock(notif)
+ onFinished: notif.modelData.unlock(notif)
Anim {
target: notif
diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml
index 019e922..2d4590e 100644
--- a/modules/notifications/Content.qml
+++ b/modules/notifications/Content.qml
@@ -55,7 +55,7 @@ Item {
id: list
model: ScriptModel {
- values: Notifs.popups.filter(n => !n.closed).reverse()
+ values: Notifs.popups.filter(n => !n.closed)
}
anchors.fill: parent
diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml
index 0a6eeaa..e3267c3 100644
--- a/modules/sidebar/NotifDock.qml
+++ b/modules/sidebar/NotifDock.qml
@@ -89,133 +89,23 @@ Item {
}
}
- StyledListView {
+ StyledFlickable {
id: view
anchors.fill: parent
- spacing: Appearance.spacing.small
-
- model: ScriptModel {
- values: {
- const list = Notifs.list.filter(n => !n.closed).sort((a, b) => b.time - a.time).map(n => [n.appName, null]);
- return [...new Map(list).keys()];
- }
- }
+ flickableDirection: Flickable.VerticalFlick
+ contentWidth: width
+ contentHeight: notifList.implicitHeight
StyledScrollBar.vertical: StyledScrollBar {
flickable: view
}
- delegate: MouseArea {
- id: notif
-
- required property int index
- required property string modelData
-
- property int startY
-
- function closeAll(): void {
- for (const n of Notifs.list.filter(n => !n.closed && n.appName === modelData))
- n.close();
- }
-
- implicitWidth: root.width
- implicitHeight: notifInner.implicitHeight
-
- hoverEnabled: true
- cursorShape: pressed ? Qt.ClosedHandCursor : undefined
- acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
- preventStealing: true
+ NotifDockList {
+ id: notifList
- drag.target: this
- drag.axis: Drag.XAxis
-
- onPressed: event => {
- if (event.button === Qt.LeftButton)
- startY = event.y;
- else if (event.button === Qt.RightButton)
- notifInner.toggleExpand();
- else if (event.button === Qt.MiddleButton)
- closeAll();
- }
- onPositionChanged: event => {
- if (pressed) {
- const diffY = event.y - startY;
- if (Math.abs(diffY) > Config.notifs.expandThreshold)
- notifInner.toggleExpand(diffY > 0);
- }
- }
- onReleased: event => {
- if (Math.abs(x) < width * Config.notifs.clearThreshold)
- x = 0;
- else
- closeAll();
- }
-
- NotifGroup {
- id: notifInner
-
- modelData: notif.modelData
- props: root.props
- }
-
- Behavior on x {
- Anim {
- duration: Appearance.anim.durations.expressiveDefaultSpatial
- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
- }
- }
- }
-
- add: Transition {
- Anim {
- property: "opacity"
- from: 0
- to: 1
- }
- Anim {
- property: "scale"
- from: 0
- to: 1
- duration: Appearance.anim.durations.expressiveDefaultSpatial
- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
- }
- }
-
- remove: Transition {
- Anim {
- property: "opacity"
- to: 0
- }
- Anim {
- property: "scale"
- to: 0.6
- }
- }
-
- move: Transition {
- Anim {
- properties: "opacity,scale"
- to: 1
- }
- Anim {
- property: "y"
- duration: Appearance.anim.durations.expressiveDefaultSpatial
- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
- }
- }
-
- displaced: Transition {
- Anim {
- properties: "opacity,scale"
- to: 1
- }
- Anim {
- property: "y"
- duration: Appearance.anim.durations.expressiveDefaultSpatial
- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
- }
+ props: root.props
}
}
}
@@ -226,8 +116,14 @@ Item {
repeat: true
interval: 50
onTriggered: {
- if (root.notifCount > 0)
- Notifs.list.find(n => !n.closed).close();
+ let next = null;
+ for (let i = 0; i < notifList.repeater.count; i++) {
+ next = notifList.repeater.itemAt(i);
+ if (!next?.closed)
+ break;
+ }
+ if (next)
+ next.closeAll();
else
stop();
}
diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml
new file mode 100644
index 0000000..0fd3464
--- /dev/null
+++ b/modules/sidebar/NotifDockList.qml
@@ -0,0 +1,150 @@
+pragma ComponentBehavior: Bound
+
+import qs.components
+import qs.services
+import qs.config
+import Quickshell
+import QtQuick
+
+Item {
+ id: root
+
+ required property Props props
+
+ readonly property alias repeater: repeater
+ readonly property int spacing: Appearance.spacing.small
+ property bool flag
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ implicitHeight: {
+ const item = repeater.itemAt(repeater.count - 1);
+ return item ? item.y + item.implicitHeight : 0;
+ }
+
+ Repeater {
+ id: repeater
+
+ model: ScriptModel {
+ values: {
+ const list = Notifs.list.map(n => [n.appName, null]);
+ return [...new Map(list).keys()];
+ }
+ onValuesChanged: root.flagChanged()
+ }
+
+ MouseArea {
+ id: notif
+
+ required property int index
+ required property string modelData
+
+ readonly property bool closed: notifInner.notifCount === 0
+ readonly property alias nonAnimHeight: notifInner.nonAnimHeight
+ property int startY
+
+ function closeAll(): void {
+ for (const n of Notifs.notClosed.filter(n => n.appName === modelData))
+ n.close();
+ }
+
+ y: {
+ root.flag; // Force update
+ let y = 0;
+ for (let i = 0; i < index; i++) {
+ const item = repeater.itemAt(i);
+ if (!item.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.RightButton | Qt.MiddleButton
+ preventStealing: true
+
+ drag.target: this
+ drag.axis: Drag.XAxis
+
+ onPressed: event => {
+ startY = event.y;
+ if (event.button === Qt.RightButton)
+ notifInner.toggleExpand(!notifInner.expanded);
+ else if (event.button === Qt.MiddleButton)
+ closeAll();
+ }
+ onPositionChanged: event => {
+ if (pressed) {
+ const diffY = event.y - startY;
+ if (Math.abs(diffY) > Config.notifs.expandThreshold)
+ notifInner.toggleExpand(diffY > 0);
+ }
+ }
+ onReleased: event => {
+ if (Math.abs(x) < width * Config.notifs.clearThreshold)
+ x = 0;
+ else
+ closeAll();
+ }
+
+ ParallelAnimation {
+ running: true
+
+ Anim {
+ target: notif
+ property: "opacity"
+ from: 0
+ to: 1
+ }
+ Anim {
+ target: notif
+ property: "scale"
+ from: 0
+ to: 1
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
+
+ ParallelAnimation {
+ running: notif.closed
+
+ Anim {
+ target: notif
+ property: "opacity"
+ to: 0
+ }
+ Anim {
+ target: notif
+ property: "scale"
+ to: 0.6
+ }
+ }
+
+ NotifGroup {
+ id: notifInner
+
+ modelData: notif.modelData
+ props: root.props
+ }
+
+ 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/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml
index a87deb6..f0b168d 100644
--- a/modules/sidebar/NotifGroup.qml
+++ b/modules/sidebar/NotifGroup.qml
@@ -16,11 +16,17 @@ StyledRect {
required property string modelData
required property Props props
- readonly property list<var> notifs: Notifs.list.filter(notif => notif.appName === modelData).reverse()
+ readonly property list<var> notifs: Notifs.list.filter(n => n.appName === modelData)
+ readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
readonly property int urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low
+ readonly property int nonAnimHeight: {
+ const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0);
+ const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin;
+ return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2);
+ }
readonly property bool expanded: props.expandedNotifs.includes(modelData)
function toggleExpand(expand: bool): void {
@@ -32,6 +38,11 @@ StyledRect {
}
}
+ Component.onDestruction: {
+ if (notifCount === 0 && expanded)
+ props.expandedNotifs.splice(props.expandedNotifs.indexOf(modelData), 1);
+ }
+
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
@@ -133,6 +144,8 @@ StyledRect {
spacing: 0
RowLayout {
+ id: header
+
Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0
Layout.fillWidth: true
spacing: Appearance.spacing.smaller
@@ -178,7 +191,7 @@ StyledRect {
Layout.leftMargin: Appearance.padding.small / 2
animate: true
- text: root.notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
+ text: root.notifCount
color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface
font.pointSize: Appearance.font.size.small
}
@@ -198,6 +211,8 @@ StyledRect {
}
NotifGroupList {
+ id: notifList
+
props: root.props
notifs: root.notifs
expanded: root.expanded
diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml
index 3c45b0b..162c343 100644
--- a/modules/sidebar/NotifGroupList.qml
+++ b/modules/sidebar/NotifGroupList.qml
@@ -14,16 +14,23 @@ Item {
required property list<var> notifs
required property bool expanded
+ readonly property real nonAnimHeight: {
+ let h = -root.spacing;
+ for (let i = 0; i < repeater.count; i++) {
+ const item = repeater.itemAt(i);
+ if (!item.modelData.closed)
+ h += item.nonAnimHeight + root.spacing;
+ }
+ return h;
+ }
+
readonly property int spacing: Math.round(Appearance.spacing.small / 2)
property bool flag
signal requestToggleExpand(expand: bool)
Layout.fillWidth: true
- implicitHeight: {
- const item = repeater.itemAt(repeater.count - 1);
- return item ? item.y + item.implicitHeight : 0;
- }
+ implicitHeight: nonAnimHeight
Repeater {
id: repeater
@@ -145,4 +152,11 @@ Item {
}
}
}
+
+ Behavior on implicitHeight {
+ Anim {
+ duration: Appearance.anim.durations.expressiveDefaultSpatial
+ easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
+ }
+ }
}
diff --git a/services/Notifs.qml b/services/Notifs.qml
index cb52bc5..7136959 100644
--- a/services/Notifs.qml
+++ b/services/Notifs.qml
@@ -12,17 +12,23 @@ import QtQuick
Singleton {
id: root
- readonly property list<Notif> list: []
+ property list<Notif> list: []
+ readonly property list<Notif> notClosed: list.filter(n => !n.closed)
readonly property list<Notif> popups: list.filter(n => n.popup)
property alias dnd: props.dnd
property bool loaded
onListChanged: {
- if (!loaded)
- return;
+ if (loaded)
+ saveTimer.restart();
+ }
+
+ Timer {
+ id: saveTimer
- storage.setText(JSON.stringify(list.filter(n => !n.closed).map(n => ({
+ interval: 1000
+ onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({
time: n.time,
id: n.id,
summary: n.summary,
@@ -35,7 +41,7 @@ Singleton {
resident: n.resident,
hasActionIcons: n.hasActionIcons,
actions: n.actions
- }))));
+ }))))
}
PersistentProperties {
@@ -60,10 +66,11 @@ Singleton {
onNotification: notif => {
notif.tracked = true;
- root.list.push(notifComp.createObject(root, {
+ const comp = notifComp.createObject(root, {
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),
notification: notif
- }));
+ });
+ root.list = [comp, ...root.list];
}
}
@@ -75,6 +82,7 @@ Singleton {
const data = JSON.parse(text());
for (const notif of data)
root.list.push(notifComp.createObject(root, notif));
+ root.list.sort((a, b) => a.time - b.time);
root.loaded = true;
}
onLoadFailed: err => {
@@ -140,21 +148,17 @@ Singleton {
}
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
- property bool resident: notification?.resident ?? false
- property bool hasActionIcons: notification?.hasActionIcons ?? false
- property list<var> actions: notification?.actions.map(a => ({
- identifier: a.identifier,
- text: a.text,
- invoke: () => a.invoke()
- })) ?? []
+ property string id
+ property string summary
+ property string body
+ property string appIcon
+ property string appName
+ property string image
+ property real expireTimeout: Config.notifs.defaultExpireTimeout
+ property int urgency: NotificationUrgency.Normal
+ property bool resident
+ property bool hasActionIcons
+ property list<var> actions
readonly property Timer timer: Timer {
running: true
@@ -171,6 +175,50 @@ Singleton {
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 {
@@ -195,6 +243,27 @@ Singleton {
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 {