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 Item { // Notification item area id: root property var notificationObject property bool expanded: false property bool onlyNotification: false property real fontSize: Appearance.font.pixelSize.small property real padding: onlyNotification ? 0 : 8 property real dragConfirmThreshold: 70 // Drag further to discard notification property real dismissOvershoot: notificationIcon.implicitWidth + 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 ?? -1 property var parentDragDistance: qmlParent?.dragDistance ?? 0 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 implicitHeight: background.implicitHeight function processNotificationBody(body, appName) { let processedBody = body // Clean Chromium-based browsers notifications - remove first line if (appName) { const lowerApp = appName.toLowerCase() const chromiumBrowsers = [ "brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge" ] if (chromiumBrowsers.some(name => lowerApp.includes(name))) { const lines = body.split('\n\n') if (lines.length > 1 && lines[0].startsWith(' { Notifications.discardNotification(notificationObject.id); } } DragManager { // Drag manager id: dragManager anchors.fill: root anchors.leftMargin: root.expanded ? -notificationIcon.implicitWidth : 0 interactive: expanded automaticallyReset: false acceptedButtons: Qt.LeftButton | Qt.MiddleButton onClicked: (mouse) => { if (mouse.button === Qt.MiddleButton) { root.destroyWithAnimation(); } } 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(); } } NotificationAppIcon { // App icon id: notificationIcon opacity: (!onlyNotification && notificationObject.image != "" && expanded) ? 1 : 0 visible: opacity > 0 Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } image: notificationObject.image anchors.right: background.left anchors.top: background.top anchors.rightMargin: 10 } Rectangle { // Background of notification item id: background width: parent.width anchors.left: parent.left radius: Appearance.rounding.small 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 } } color: (expanded && !onlyNotification) ? (notificationObject.urgency == NotificationUrgency.Critical) ? ColorUtils.mix(Appearance.colors.colSecondaryContainer, Appearance.colors.colLayer2, 0.35) : (Appearance.colors.colSurfaceContainerHigh) : ColorUtils.transparentize(Appearance.colors.colSurfaceContainerHighest) implicitHeight: expanded ? (contentColumn.implicitHeight + padding * 2) : summaryRow.implicitHeight Behavior on implicitHeight { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } ColumnLayout { // Content column id: contentColumn anchors.fill: parent anchors.margins: expanded ? root.padding : 0 spacing: 3 Behavior on anchors.margins { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } RowLayout { // Summary row id: summaryRow visible: !root.onlyNotification || !root.expanded Layout.fillWidth: true implicitHeight: summaryText.implicitHeight // Layout.fillWidth: true StyledText { id: summaryText visible: !root.onlyNotification font.pixelSize: root.fontSize color: Appearance.colors.colOnLayer2 elide: Text.ElideRight text: root.notificationObject.summary || "" } StyledText { opacity: !root.expanded ? 1 : 0 visible: opacity > 0 Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } Layout.fillWidth: true font.pixelSize: root.fontSize color: Appearance.colors.colSubtext elide: Text.ElideRight maximumLineCount: 1 textFormat: Text.StyledText text: { return processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
") } } } ColumnLayout { // Expanded content Layout.fillWidth: true opacity: root.expanded ? 1 : 0 visible: opacity > 0 StyledText { // Notification body (expanded) id: notificationBodyText Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } Layout.fillWidth: true font.pixelSize: root.fontSize color: Appearance.colors.colSubtext wrapMode: Text.Wrap elide: Text.ElideRight textFormat: Text.RichText text: { return `` + `${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "
")}` } onLinkActivated: (link) => { Qt.openUrlExternally(link) Hyprland.dispatch("global quickshell:sidebarRightClose") } PointingHandLinkHover {} } Flickable { // Notification actions id: actionsFlickable Layout.fillWidth: true implicitHeight: actionRowLayout.implicitHeight contentWidth: actionRowLayout.implicitWidth clip: !onlyNotification Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } Behavior on height { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } Behavior on implicitHeight { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } RowLayout { id: actionRowLayout Layout.alignment: Qt.AlignBottom NotificationActionButton { Layout.fillWidth: true buttonText: qsTr("Close") urgency: notificationObject.urgency implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) : (contentItem.implicitWidth + leftPadding + rightPadding) onClicked: { root.destroyWithAnimation() } contentItem: MaterialSymbol { iconSize: Appearance.font.pixelSize.large horizontalAlignment: Text.AlignHCenter color: (notificationObject.urgency == NotificationUrgency.Critical) ? Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface text: "close" } } 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 urgency: notificationObject.urgency implicitWidth: (notificationObject.actions.length == 0) ? ((actionsFlickable.width - actionRowLayout.spacing) / 2) : (contentItem.implicitWidth + leftPadding + rightPadding) onClicked: { Quickshell.clipboardText = notificationObject.body copyIcon.text = "inventory" copyIconTimer.restart() } Timer { id: copyIconTimer interval: 1500 repeat: false onTriggered: { copyIcon.text = "content_copy" } } contentItem: MaterialSymbol { id: copyIcon iconSize: Appearance.font.pixelSize.large horizontalAlignment: Text.AlignHCenter color: (notificationObject.urgency == NotificationUrgency.Critical) ? Appearance.m3colors.m3onSurfaceVariant : Appearance.m3colors.m3onSurface text: "content_copy" } } } } } } } }