quickshell-toki-night/modules/notifications/Notification.qml

599 lines
19 KiB
QML

pragma ComponentBehavior: Bound
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
CustomRect {
id: root
required property Notifs.Notif notif
readonly property bool hasImage: notif.image.length > 0
readonly property bool hasAppIcon: notif.appIcon.length > 0
readonly property int nonAnimHeight: inner.nonAnimHeight + inner.anchors.margins * 2
property bool inPopup: true
property bool expanded: false
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.errorBg : Config.colors.container
radius: 12
implicitWidth: Config.notifs.width
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
onExpandedChanged: {
if (root.inPopup && expanded)
root.notif.timer.stop();
}
x: inPopup ? Config.notifs.width : 0
Component.onCompleted: x = 0
Behavior on x {
Anim {
easing.bezierCurve: Config.anim.curves.emphasizedDecel
}
}
RetainableLock {
object: root.notif.notification
locked: true
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
acceptedButtons: Qt.AllButtons
preventStealing: true
onEntered: {
if (root.inPopup)
root.notif.timer.stop();
}
onExited: {
if (root.inPopup && !pressed && !root.expanded)
root.notif.timer.start();
}
drag.target: parent
drag.axis: Drag.XAxis
onPressed: event => {
if (event.button === Qt.MiddleButton)
root.notif.notification.dismiss();
}
onReleased: event => {
if (root.inPopup && !containsMouse && !root.expanded)
root.notif.timer.start();
if (Math.abs(root.x) < Config.notifs.width * Config.notifs.clearThreshold)
root.x = 0;
else if (root.inPopup)
root.notif.popup = null;
else
root.notif.notification.dismiss();
}
onClicked: event => {
if (event.button === Qt.LeftButton) {
const actions = root.notif.actions;
if (actions?.length === 1)
actions[0].invoke();
else
root.expanded = !root.expanded;
} else if (event.button === Qt.RightButton) {
root.expanded = !root.expanded;
}
}
Item {
id: inner
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
readonly property real nonAnimHeight: root.expanded ? summary.height + appName.height + body.height + actions.height + 16
: Config.notifs.imageSize
implicitHeight: nonAnimHeight
Behavior on implicitHeight {
Anim {
duration: Config.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Config.anim.curves.expressiveDefaultSpatial
}
}
Loader {
id: image
active: root.hasImage
asynchronous: true
anchors.left: parent.left
anchors.top: parent.top
width: Config.notifs.imageSize
height: Config.notifs.imageSize
visible: root.hasImage || root.hasAppIcon
sourceComponent: ClippingRectangle {
radius: 1000
implicitWidth: Config.notifs.imageSize
implicitHeight: Config.notifs.imageSize
Image {
anchors.fill: parent
source: Qt.resolvedUrl(root.notif.image)
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
}
}
Loader {
id: appIcon
active: root.hasAppIcon || !root.hasImage
asynchronous: true
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
anchors.right: root.hasImage ? image.right : undefined
anchors.bottom: root.hasImage ? image.bottom : undefined
sourceComponent: CustomRect {
radius: 1000
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.error
: root.notif.urgency === NotificationUrgency.Low ? Config.colors.inactive
: Config.colors.notification
implicitWidth: root.hasImage ? Config.notifs.badgeSize : Config.notifs.imageSize
implicitHeight: root.hasImage ? Config.notifs.badgeSize : Config.notifs.imageSize
Loader {
id: icon
active: root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
width: Math.round(parent.width * 0.6)
height: Math.round(parent.width * 0.6)
sourceComponent: IconImage {
anchors.fill: parent
source: Quickshell.iconPath(root.notif.appIcon)
asynchronous: true
}
}
Loader {
active: !root.hasAppIcon
asynchronous: true
anchors.centerIn: parent
sourceComponent: MaterialIcon {
text: Icons.getNotifIcon(root.notif.summary, root.notif.urgency)
color: root.notif.urgency === NotificationUrgency.Low ? Config.colors.primary
: Config.colors.primaryDark
font.pointSize: Config.font.size.larger
}
}
}
}
CustomText {
id: appName
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: 10
animate: true
text: appNameMetrics.elidedText
maximumLineCount: 1
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.primary : Config.colors.tertiary
font.pointSize: Config.font.size.small
opacity: root.expanded ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: appNameMetrics
text: root.notif.appName
font.family: appName.font.family
font.pointSize: appName.font.pointSize
elide: Text.ElideRight
elideWidth: closeBtn.x - timeDetail.width - time.width - timeSep.width - summaryPreview.x - 21
}
CustomText {
id: summaryPreview
anchors.top: parent.top
anchors.left: image.right
anchors.leftMargin: 10
anchors.topMargin: 2
animate: true
text: summaryPreviewMetrics.elidedText
color: root.notif.urgency === NotificationUrgency.Low ? Config.colors.primary : Config.colors.secondary
opacity: root.expanded ? 0 : 1
visible: opacity > 0
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: summaryPreview
anchors.top: appName.bottom
}
}
transitions: Transition {
AnchorAnimation {
duration: Config.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Config.anim.curves.standard
}
}
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: summaryPreviewMetrics
text: root.notif.summary
font.family: summaryPreview.font.family
font.pointSize: summaryPreview.font.pointSize
elide: Text.ElideRight
elideWidth: closeBtn.x - time.width - timeSep.width - summaryPreview.x - 21
}
CustomText {
id: summary
anchors.top: parent.top
anchors.left: image.right
anchors.right: parent.right
anchors.leftMargin: 10
anchors.topMargin: 5
anchors.rightMargin: 10
animate: true
text: root.notif.summary
color: root.notif.urgency === NotificationUrgency.Low ? Config.colors.primary : Config.colors.secondary
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: summary
anchors.top: appName.bottom
}
}
transitions: Transition {
AnchorAnimation {
duration: Config.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Config.anim.curves.standard
}
}
opacity: root.expanded ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
CustomText {
id: timeSep
anchors.top: parent.top
anchors.left: summaryPreview.right
anchors.leftMargin: 7
anchors.topMargin: root.expanded ? 0 : 3
text: "•"
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.primary : Config.colors.tertiary
font.pointSize: Config.font.size.small
states: State {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timeSep
anchors.left: appName.right
}
}
transitions: Transition {
AnchorAnimation {
duration: Config.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Config.anim.curves.standard
}
}
}
CustomText {
id: time
anchors.top: parent.top
anchors.left: timeSep.right
anchors.leftMargin: 7
anchors.topMargin: root.expanded ? 0 : 2
animate: true
horizontalAlignment: Text.AlignLeft
text: root.notif.timeSince
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.primary : Config.colors.tertiary
font.pointSize: Config.font.size.small
}
CustomText {
id: timeDetail
anchors.verticalCenter: time.verticalCenter
anchors.left: time.right
anchors.leftMargin: 5
horizontalAlignment: Text.AlignLeft
text: `(${root.notif.timeStr})`
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.primary : Config.colors.tertiary
font.pointSize: Config.font.size.small
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
}
StateLayer {
id: closeBtn
anchors.right: expandBtn.left
anchors.top: parent.top
implicitWidth: 20
implicitHeight: 20
function onClicked() {
root.notif.notification.dismiss();
}
MaterialIcon {
id: closeIcon
anchors.centerIn: parent
animate: true
text: "close"
font.pointSize: Config.font.size.normal
}
}
StateLayer {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: 20
implicitHeight: 20
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.secondary : Config.colors.primary
radius: 1000
function onClicked() {
root.expanded = !root.expanded
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.verticalCenterOffset: text === "expand_more" ? 1 : 0
animate: true
text: root.expanded ? "expand_less" : "expand_more"
font.pointSize: Config.font.size.normal
}
}
CustomText {
id: bodyPreview
anchors.left: summaryPreview.left
anchors.right: parent.right
anchors.top: summaryPreview.bottom
anchors.topMargin: 3
anchors.rightMargin: 7
animate: true
textFormat: Text.MarkdownText
text: bodyPreviewMetrics.elidedText
color: root.notif.urgency === NotificationUrgency.Low ? Config.colors.tertiary : Config.colors.primary
font.pointSize: Config.font.size.small
opacity: root.expanded ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
TextMetrics {
id: bodyPreviewMetrics
text: root.notif.bodyOneLine
font.family: bodyPreview.font.family
font.pointSize: bodyPreview.font.pointSize
elide: Text.ElideRight
elideWidth: bodyPreview.width
}
CustomText {
id: body
anchors.left: summary.left
anchors.right: parent.right
anchors.top: summary.bottom
anchors.rightMargin: 7
anchors.topMargin: 3
animate: true
textFormat: Text.MarkdownText
text: root.notif.body
color: root.notif.urgency === NotificationUrgency.Low ? Config.colors.tertiary : Config.colors.primary
font.pointSize: Config.font.size.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
height: text ? implicitHeight : 0
onLinkActivated: link => {
if (!root.expanded)
return;
Quickshell.execDetached(["xdg-open", link]);
root.notif.notification.dismiss(); // TODO: change back to popup when notif dock impled
}
opacity: root.expanded ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
RowLayout {
id: actions
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: body.bottom
anchors.topMargin: 10
spacing: 10
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {}
}
Action {
modelData: QtObject {
readonly property string text: qsTr("Close")
function invoke(): void {
root.notif.notification.dismiss();
}
}
}
Repeater {
model: root.notif.actions
delegate: Component {
Action {}
}
}
}
}
}
CustomRect {
id: progressBar
visible: root.inPopup
anchors.bottom: parent.bottom
height: 2
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.error : Config.colors.inactive
opacity: root.notif.timer.running ? 1 : 0
NumberAnimation on implicitWidth {
from: root.width
to: 0
duration: Config.notifs.defaultExpireTimeout
running: root.notif.timer.running
}
Behavior on opacity {
Anim { duration: Config.anim.durations.small }
}
}
component Action: CustomRect {
id: action
required property var modelData
radius: 1000
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.error : Config.colors.inactive
implicitWidth: actionText.width + 20
implicitHeight: actionText.height + 10
Layout.preferredWidth: implicitWidth
Layout.preferredHeight: implicitHeight
StateLayer {
anchors.fill: parent
radius: 1000
color: root.notif.urgency === NotificationUrgency.Critical ? "#ffffff" : Config.colors.primary
function onClicked(): void {
action.modelData.invoke();
}
}
CustomText {
id: actionText
anchors.centerIn: parent
text: actionTextMetrics.elidedText
color: root.notif.urgency === NotificationUrgency.Critical ? Config.colors.primaryDark
: root.notif.urgency === NotificationUrgency.Low ? Config.colors.primary : Config.colors.secondary
font.pointSize: Config.font.size.small
}
TextMetrics {
id: actionTextMetrics
text: action.modelData.text
font.family: actionText.font.family
font.pointSize: actionText.font.pointSize
elide: Text.ElideRight
elideWidth: {
const numActions = root.notif.actions.length + 1;
return (inner.width - actions.spacing * (numActions - 1)) / numActions - 20;
}
}
}
}