diff options
| -rw-r--r-- | modules/lock/NotifDock.qml | 5 | ||||
| -rw-r--r-- | modules/lock/NotifGroup.qml | 4 | ||||
| -rw-r--r-- | modules/notifications/Content.qml | 2 | ||||
| -rw-r--r-- | modules/sidebar/NotifDock.qml | 134 | ||||
| -rw-r--r-- | modules/sidebar/NotifDockList.qml | 150 | ||||
| -rw-r--r-- | modules/sidebar/NotifGroup.qml | 19 | ||||
| -rw-r--r-- | modules/sidebar/NotifGroupList.qml | 22 | ||||
| -rw-r--r-- | services/Notifs.qml | 113 |
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 { |