forked from Shinonome/dots-hyprland
notifications: groups
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import "root:/modules/common"
|
||||
import "root:/services"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import "root:/modules/common/functions/color_utils.js" as ColorUtils
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
/**
|
||||
* A group of notifications from the same app.
|
||||
* Similar to Android's notifications
|
||||
*/
|
||||
Item { // Notification group area
|
||||
id: root
|
||||
property var notificationGroup
|
||||
property var notifications: notificationGroup.notifications
|
||||
property int notificationCount: notifications.length
|
||||
property bool multipleNotifications: notificationCount > 1
|
||||
property bool expanded: false
|
||||
property bool popup: false
|
||||
property real padding: 10
|
||||
implicitHeight: background.implicitHeight
|
||||
|
||||
property real dragConfirmThreshold: 70 // Drag further to discard notification
|
||||
property real dismissOvershoot: 20 // Account for gaps and bouncy animations
|
||||
property var qmlParent: root.parent.parent // There's something between this and the parent ListView
|
||||
property var parentDragIndex: qmlParent.dragIndex
|
||||
property var parentDragDistance: qmlParent.dragDistance
|
||||
property var dragIndexDiff: Math.abs(parentDragIndex - index)
|
||||
property real xOffset: dragIndexDiff == 0 ? Math.max(0, parentDragDistance) :
|
||||
parentDragDistance > dragConfirmThreshold ? 0 :
|
||||
dragIndexDiff == 1 ? Math.max(0, parentDragDistance * 0.3) :
|
||||
dragIndexDiff == 2 ? Math.max(0, parentDragDistance * 0.1) : 0
|
||||
|
||||
function destroyWithAnimation() {
|
||||
root.qmlParent.resetDrag()
|
||||
background.anchors.leftMargin = background.anchors.leftMargin; // Break binding
|
||||
destroyAnimation.running = true;
|
||||
}
|
||||
|
||||
SequentialAnimation { // Drag finish animation
|
||||
id: destroyAnimation
|
||||
running: false
|
||||
|
||||
NumberAnimation {
|
||||
target: background.anchors
|
||||
property: "leftMargin"
|
||||
to: root.width + root.dismissOvershoot
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
onFinished: () => {
|
||||
root.notifications.forEach((notif) => {
|
||||
Notifications.discardNotification(notif.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpanded() {
|
||||
if (expanded) implicitHeightAnim.enabled = true;
|
||||
else implicitHeightAnim.enabled = false;
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
|
||||
DragManager { // Drag manager
|
||||
id: dragManager
|
||||
anchors.fill: parent
|
||||
interactive: !expanded
|
||||
automaticallyReset: false
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.toggleExpanded();
|
||||
}
|
||||
}
|
||||
|
||||
onDraggingChanged: () => {
|
||||
if (dragging) {
|
||||
root.qmlParent.dragIndex = root.index ?? root.parent.children.indexOf(root);
|
||||
}
|
||||
}
|
||||
|
||||
onDragDiffXChanged: () => {
|
||||
root.qmlParent.dragDistance = dragDiffX;
|
||||
}
|
||||
|
||||
onDragReleased: (diffX, diffY) => {
|
||||
if (diffX > root.dragConfirmThreshold)
|
||||
root.destroyWithAnimation();
|
||||
else
|
||||
dragManager.resetDrag();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Rectangle { // Background of the notification
|
||||
id: background
|
||||
anchors.left: parent.left
|
||||
width: parent.width
|
||||
color: Appearance.m3colors.m3surfaceContainer
|
||||
radius: Appearance.rounding.normal
|
||||
anchors.leftMargin: root.xOffset
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
enabled: !dragManager.dragging
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animationCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
implicitHeight: expanded ?
|
||||
row.implicitHeight + padding * 2 :
|
||||
Math.min(80, row.implicitHeight + padding * 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
id: implicitHeightAnim
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
source: background
|
||||
anchors.fill: background
|
||||
shadowEnabled: popup
|
||||
shadowColor: Appearance.colors.colShadow
|
||||
shadowVerticalOffset: 1
|
||||
shadowBlur: 0.5
|
||||
}
|
||||
|
||||
RowLayout { // Left column for icon, right column for content
|
||||
id: row
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: root.padding
|
||||
spacing: 10
|
||||
|
||||
NotificationAppIcon { // Icons
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: false
|
||||
|
||||
appIcon: notificationGroup.appIcon
|
||||
summary: notificationGroup.notifications[root.notificationCount - 1].summary
|
||||
}
|
||||
|
||||
ColumnLayout { // Content
|
||||
Layout.fillWidth: true
|
||||
spacing: expanded ? 5 : 0
|
||||
Behavior on spacing {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
RowLayout { // App name (or summary when there's only 1 notif) and time
|
||||
id: topRow
|
||||
spacing: 0
|
||||
property real fontSize: Appearance.font.pixelSize.smaller
|
||||
property bool showAppName: root.multipleNotifications
|
||||
|
||||
StyledText {
|
||||
id: appName
|
||||
text: topRow.showAppName ?
|
||||
notificationGroup.appName :
|
||||
notificationGroup.notifications[0].summary
|
||||
font.pixelSize: topRow.showAppName ?
|
||||
topRow.fontSize :
|
||||
Appearance.font.pixelSize.small
|
||||
color: topRow.showAppName ?
|
||||
Appearance.colors.colSubtext :
|
||||
Appearance.colors.colOnLayer2
|
||||
}
|
||||
StyledText {
|
||||
id: timeText
|
||||
text: " • " + NotificationUtils.getFriendlyNotifTimeString(notificationGroup.time)
|
||||
font.pixelSize: topRow.fontSize
|
||||
color: Appearance.colors.colSubtext
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
NotificationGroupExpandButton {
|
||||
count: root.notificationCount
|
||||
expanded: root.expanded
|
||||
fontSize: topRow.fontSize
|
||||
onClicked: { root.toggleExpanded() }
|
||||
}
|
||||
}
|
||||
|
||||
StyledListView { // Notification body (expanded)
|
||||
id: notificationsColumn
|
||||
implicitHeight: contentHeight
|
||||
Layout.fillWidth: true
|
||||
spacing: expanded ? 5 : 3
|
||||
// clip: true
|
||||
interactive: false
|
||||
Behavior on spacing {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
model: ScriptModel {
|
||||
// values: root.expanded ? root.notifications : root.notifications.slice(0, 2)
|
||||
values: root.notifications.slice().reverse()
|
||||
}
|
||||
delegate: NotificationItem {
|
||||
required property int index
|
||||
required property var modelData
|
||||
notificationObject: modelData
|
||||
expanded: root.expanded
|
||||
onlyNotification: (root.notificationCount === 1)
|
||||
opacity: (!root.expanded && index == 1 && root.notificationCount > 2) ? 0.5 : 1
|
||||
visible: root.expanded || (index < 2)
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user