import "root:/modules/common" import "root:/services" import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications import "./notification_utils.js" as NotificationUtils Item { id: root property var notificationObject property bool expanded: false property bool enableAnimation: true property int notificationListSpacing: 5 property bool ready: false Layout.fillWidth: true clip: true implicitHeight: ready ? notificationColumnLayout.implicitHeight + notificationListSpacing : 0 Behavior on implicitHeight { enabled: enableAnimation NumberAnimation { duration: Appearance.animation.elementDecelFast.duration easing.type: Appearance.animation.elementDecel.type } } Component.onCompleted: { root.ready = true } function destroyWithAnimation(delay = 0) { destroyTimer0.interval = delay destroyTimer0.start() } function toggleExpanded() { root.enableAnimation = true notificationRowWrapper.anchors.bottom = undefined root.expanded = !root.expanded } Timer { id: destroyTimer0 interval: 0 repeat: false onTriggered: { notificationRowWrapper.anchors.left = undefined notificationRowWrapper.anchors.right = undefined notificationRowWrapper.anchors.fill = undefined notificationBackground.anchors.left = undefined notificationBackground.anchors.right = undefined notificationBackground.anchors.fill = undefined notificationRowWrapper.x = width notificationBackground.x = width destroyTimer1.start() } } Timer { id: destroyTimer1 interval: Appearance.animation.elementDecelFast.duration repeat: false onTriggered: { notificationRowWrapper.anchors.top = undefined notificationRowWrapper.anchors.bottom = root.bottom implicitHeight = 0 destroyTimer2.start() } } Timer { id: destroyTimer2 interval: Appearance.animation.elementDecelFast.duration repeat: false onTriggered: { root.destroy() } } MouseArea { // Middle click to close anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton onClicked: (mouse) => { if (mouse.button == Qt.MiddleButton) Notifications.discardNotification(notificationObject.id); else if (mouse.button == Qt.RightButton) root.toggleExpanded() } // Flick right to dismiss property real startX: 0 property real dragStartThreshold: 10 property real dragConfirmThresholdRatio: 0.2 property bool dragStarted: false onPressed: (mouse) => { if (mouse.button === Qt.LeftButton) { startX = mouse.x } } onDragStartedChanged: () => { // Prevent drag focus being shifted to parent flickable root.parent.parent.parent.interactive = !dragStarted } onReleased: (mouse) => { dragStarted = false if (mouse.button === Qt.LeftButton) { if (notificationRowWrapper.x > width * dragConfirmThresholdRatio) { Notifications.discardNotification(notificationObject.id); } else { // Animate back if not far enough notificationRowWrapper.x = 0 notificationBackground.x = 0 } } } onPositionChanged: (mouse) => { if (mouse.buttons & Qt.LeftButton) { let dx = mouse.x - startX if (dragStarted || dx > dragStartThreshold) { dragStarted = true notificationRowWrapper.anchors.left = undefined notificationRowWrapper.anchors.right = undefined notificationRowWrapper.anchors.fill = undefined notificationBackground.anchors.left = undefined notificationBackground.anchors.right = undefined notificationBackground.anchors.fill = undefined notificationRowWrapper.x = Math.max(0, dx) notificationBackground.x = Math.max(0, dx) } } } } // Background Item { id: notificationBackgroundWrapper // anchors.fill: parent anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom // anchors.top: parent.top anchors.topMargin: notificationListSpacing implicitHeight: notificationColumnLayout.implicitHeight + notificationListSpacing Rectangle { id: notificationBackground anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom // anchors.top: parent.top height: notificationColumnLayout.implicitHeight color: (notificationObject.urgency == NotificationUrgency.Critical) ? Appearance.mix(Appearance.m3colors.m3secondaryContainer, Appearance.colors.colLayer2, 0.35) : Appearance.colors.colLayer2 radius: Appearance.rounding.normal Behavior on x { enabled: enableAnimation NumberAnimation { duration: Appearance.animation.elementDecelFast.duration easing.type: Appearance.animation.elementDecel.type } } Behavior on height { enabled: enableAnimation NumberAnimation { duration: Appearance.animation.elementDecelFast.duration easing.type: Appearance.animation.elementDecel.type } } } } Item { id: notificationRowWrapper anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom // anchors.top: parent.top implicitHeight: notificationColumnLayout.implicitHeight + notificationListSpacing Behavior on x { enabled: enableAnimation NumberAnimation { duration: Appearance.animation.elementDecelFast.duration easing.type: Appearance.animation.elementDecel.type } } ColumnLayout { id: notificationColumnLayout anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom spacing: 0 Item { Layout.fillWidth: true implicitHeight: notificationRowLayout.implicitHeight Behavior on implicitHeight { enabled: enableAnimation NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } RowLayout { id: notificationRowLayout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Rectangle { // App icon id: iconRectangle implicitWidth: 47 implicitHeight: 47 Layout.leftMargin: 10 Layout.topMargin: 10 Layout.bottomMargin: 10 Layout.alignment: Qt.AlignTop Layout.fillWidth: false radius: Appearance.rounding.full color: Appearance.m3colors.m3secondaryContainer MaterialSymbol { visible: notificationObject.appIcon == "" text: (notificationObject.urgency == NotificationUrgency.Critical) ? "release_alert" : NotificationUtils.guessMessageType(notificationObject.summary) anchors.fill: parent color: (notificationObject.urgency == NotificationUrgency.Critical) ? Appearance.mix(Appearance.m3colors.m3onSecondary, Appearance.m3colors.m3onSecondaryContainer, 0.1) : Appearance.m3colors.m3onSecondaryContainer font.pixelSize: 27 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } IconImage { visible: notificationObject.image == "" && notificationObject.appIcon != "" anchors.centerIn: parent implicitSize: 33 asynchronous: true source: Quickshell.iconPath(notificationObject.appIcon) } Item { anchors.fill: parent visible: notificationObject.image != "" Image { id: notifImage anchors.fill: parent readonly property int size: parent.width source: notificationObject?.image fillMode: Image.PreserveAspectCrop cache: false antialiasing: true asynchronous: true width: size height: size sourceSize.width: size sourceSize.height: size layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: notifImage.size height: notifImage.size radius: Appearance.rounding.full } } } IconImage { visible: notificationObject.appIcon != "" anchors.bottom: parent.bottom anchors.right: parent.right implicitSize: 23 asynchronous: true source: Quickshell.iconPath(notificationObject.appIcon) } } } ColumnLayout { // Notification content spacing: 0 Layout.fillWidth: true RowLayout { // Row of summary, time and expand button Layout.topMargin: 10 Layout.leftMargin: 10 Layout.rightMargin: 10 Layout.fillWidth: true StyledText { // Summary Layout.fillWidth: true horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignBottom font.pixelSize: Appearance.font.pixelSize.normal color: Appearance.colors.colOnLayer2 text: notificationObject.summary wrapMode: expanded ? Text.Wrap : Text.NoWrap elide: Text.ElideRight } // StyledText { // dot // id: notificationDot // Layout.fillWidth: false // wrapMode: Text.Wrap // horizontalAlignment: Text.AlignLeft // font.pixelSize: Appearance.font.pixelSize.smaller // color: Appearance.m3colors.m3outline // text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) // } StyledText { // Time id: notificationTimeText Layout.fillWidth: false Layout.alignment: Qt.AlignTop Layout.topMargin: 3 wrapMode: Text.Wrap horizontalAlignment: Text.AlignLeft font.pixelSize: Appearance.font.pixelSize.smaller color: Appearance.m3colors.m3outline text: NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) Connections { target: DateTime function onTimeChanged() { notificationTimeText.text = NotificationUtils.getFriendlyNotifTimeString(notificationObject.time) } } } Item { Layout.fillWidth: true } Button { // Expand button Layout.alignment: Qt.AlignTop id: expandButton implicitWidth: 22 implicitHeight: 22 PointingHandInteraction{} onClicked: { root.toggleExpanded() } background: Rectangle { anchors.fill: parent radius: Appearance.rounding.full color: (expandButton.down) ? Appearance.colors.colLayer2Active : (expandButton.hovered ? Appearance.colors.colLayer2Hover : Appearance.transparentize(Appearance.colors.colLayer2, 1)) Behavior on color { ColorAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } } contentItem: MaterialSymbol { anchors.centerIn: parent // text: expanded ? "keyboard_arrow_up" : "keyboard_arrow_down" text: "keyboard_arrow_down" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: Appearance.font.pixelSize.normal color: Appearance.colors.colOnLayer2 rotation: expanded ? 180 : 0 Behavior on rotation { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } } } } StyledText { // Notification body Layout.fillWidth: true Layout.leftMargin: 10 Layout.rightMargin: 10 Layout.bottomMargin: 10 clip: true wrapMode: expanded ? Text.Wrap : Text.NoWrap elide: Text.ElideRight font.pixelSize: Appearance.font.pixelSize.small horizontalAlignment: Text.AlignLeft color: Appearance.m3colors.m3outline // textFormat: Text.MarkdownText text: expanded ? notificationObject.body : notificationObject.body.split("\n")[0] } } } } // Actions Flickable { id: actionsFlickable Layout.fillWidth: true // Layout.topMargin: -5 Layout.leftMargin: 10 Layout.rightMargin: 10 Layout.bottomMargin: expanded ? 10 : 0 implicitHeight: expanded ? actionRowLayout.implicitHeight : 0 height: expanded ? actionRowLayout.implicitHeight : 0 contentWidth: actionRowLayout.implicitWidth clip: true opacity: expanded ? 1 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } Behavior on height { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } Behavior on implicitHeight { NumberAnimation { duration: Appearance.animation.elementDecel.duration easing.type: Appearance.animation.elementDecel.type } } RowLayout { id: actionRowLayout Layout.alignment: Qt.AlignBottom Repeater { id: actionRepeater model: notificationObject.actions NotificationActionButton { Layout.fillWidth: true buttonText: modelData.text urgency: notificationObject.urgency onClicked: { Notifications.attemptInvokeAction(notificationObject.id, modelData.identifier); } } } NotificationActionButton { Layout.fillWidth: true buttonText: "Close" urgency: notificationObject.urgency implicitWidth: (notificationObject.actions.length == 0) ? (actionsFlickable.width) : (contentItem.implicitWidth + leftPadding + rightPadding) onClicked: { Notifications.discardNotification(notificationObject.id); } } } } } } }