mirror of
https://github.com/celesrenata/end-4-flakes.git
synced 2026-06-06 10:49:26 -05:00
314 lines
13 KiB
QML
314 lines
13 KiB
QML
import qs
|
|
import qs.modules.common
|
|
import qs.services
|
|
import qs.modules.common.functions
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
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('<a')) {
|
|
processedBody = lines.slice(1).join('\n\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
return processedBody
|
|
}
|
|
|
|
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: () => {
|
|
Notifications.discardNotification(notificationObject.notificationId);
|
|
}
|
|
}
|
|
|
|
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
|
|
Layout.fillWidth: true
|
|
Behavior on opacity {
|
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
|
}
|
|
font.pixelSize: root.fontSize
|
|
color: Appearance.colors.colSubtext
|
|
elide: Text.ElideRight
|
|
wrapMode: Text.Wrap // Needed for proper eliding????
|
|
maximumLineCount: 1
|
|
textFormat: Text.StyledText
|
|
text: {
|
|
return processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "<br/>")
|
|
}
|
|
}
|
|
}
|
|
|
|
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 `<style>img{max-width:${300 /* binding to notificationBodyText.width would cause a binding loop */}px;}</style>` +
|
|
`${processNotificationBody(notificationObject.body, notificationObject.appName || notificationObject.summary).replace(/\n/g, "<br/>")}`
|
|
}
|
|
|
|
onLinkActivated: (link) => {
|
|
Qt.openUrlExternally(link)
|
|
GlobalStates.sidebarRightOpen = false
|
|
}
|
|
|
|
PointingHandLinkHover {}
|
|
}
|
|
|
|
StyledFlickable { // 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: Translation.tr("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.notificationId, 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"
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|