521 lines
19 KiB
QML
521 lines
19 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Shapes
|
|
import Quickshell
|
|
import Quickshell.Services.Notifications
|
|
import Caelestia.Components
|
|
import Caelestia.Config
|
|
import qs.components
|
|
import qs.components.controls
|
|
import qs.components.effects
|
|
import qs.services
|
|
import qs.utils
|
|
|
|
StyledRect {
|
|
id: root
|
|
|
|
required property NotifData modelData
|
|
readonly property bool hasImage: modelData.image.length > 0
|
|
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
|
readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText
|
|
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
|
property bool expanded: Config.notifs.openExpanded
|
|
|
|
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer
|
|
radius: Tokens.rounding.large
|
|
implicitWidth: Tokens.sizes.notifs.width
|
|
implicitHeight: inner.implicitHeight
|
|
|
|
x: Tokens.sizes.notifs.width
|
|
Component.onCompleted: {
|
|
x = 0;
|
|
modelData.lock(this);
|
|
}
|
|
Component.onDestruction: modelData.unlock(this)
|
|
|
|
Behavior on x {
|
|
Anim {
|
|
easing: Tokens.anim.emphasizedDecel
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
property int startY
|
|
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
|
preventStealing: true
|
|
|
|
onEntered: root.modelData.timer.stop()
|
|
onExited: {
|
|
if (!pressed)
|
|
root.modelData.timer.start();
|
|
}
|
|
|
|
drag.target: parent
|
|
drag.axis: Drag.XAxis
|
|
|
|
onPressed: event => {
|
|
root.modelData.timer.stop();
|
|
startY = event.y;
|
|
if (event.button === Qt.MiddleButton)
|
|
root.modelData.close();
|
|
}
|
|
onReleased: event => {
|
|
if (!containsMouse)
|
|
root.modelData.timer.start();
|
|
|
|
if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold)
|
|
root.x = 0;
|
|
else
|
|
root.modelData.popup = false;
|
|
}
|
|
onPositionChanged: event => {
|
|
if (pressed) {
|
|
const diffY = event.y - startY;
|
|
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
|
root.expanded = diffY > 0;
|
|
}
|
|
}
|
|
onClicked: event => {
|
|
if (!GlobalConfig.notifs.actionOnClick || event.button !== Qt.LeftButton)
|
|
return;
|
|
|
|
const actions = root.modelData.actions;
|
|
if (actions.length === 1)
|
|
actions[0].invoke();
|
|
}
|
|
|
|
Item {
|
|
id: inner
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Tokens.padding.medium
|
|
|
|
implicitHeight: root.nonAnimHeight
|
|
|
|
Behavior on implicitHeight {
|
|
Anim {}
|
|
}
|
|
|
|
Loader {
|
|
id: image
|
|
|
|
asynchronous: true
|
|
active: root.hasImage
|
|
|
|
anchors.left: parent.left
|
|
anchors.top: parent.top
|
|
width: TokenConfig.sizes.notifs.image
|
|
height: TokenConfig.sizes.notifs.image
|
|
visible: root.hasImage || root.hasAppIcon
|
|
|
|
sourceComponent: StyledClippingRect {
|
|
radius: Tokens.rounding.full
|
|
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
|
|
implicitWidth: TokenConfig.sizes.notifs.image
|
|
implicitHeight: TokenConfig.sizes.notifs.image
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
source: Qt.resolvedUrl(root.modelData.image)
|
|
fillMode: Image.PreserveAspectCrop
|
|
sourceSize: {
|
|
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
|
return Qt.size(size, size);
|
|
}
|
|
cache: false
|
|
asynchronous: true
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: appIcon
|
|
|
|
asynchronous: true
|
|
active: root.hasAppIcon || !root.hasImage
|
|
|
|
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: StyledRect {
|
|
radius: Tokens.rounding.full
|
|
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer
|
|
implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
|
|
implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image
|
|
|
|
Loader {
|
|
id: icon
|
|
|
|
asynchronous: true
|
|
active: root.hasAppIcon
|
|
|
|
anchors.centerIn: parent
|
|
|
|
width: Math.round(parent.width * 0.6)
|
|
height: Math.round(parent.width * 0.6)
|
|
|
|
sourceComponent: ColouredIcon {
|
|
anchors.fill: parent
|
|
source: Quickshell.iconPath(root.modelData.appIcon)
|
|
colour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
|
|
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
asynchronous: true
|
|
active: !root.hasAppIcon
|
|
anchors.centerIn: parent
|
|
anchors.horizontalCenterOffset: -Tokens.font.body.large.pointSize * 0.02
|
|
anchors.verticalCenterOffset: Tokens.font.body.large.pointSize * 0.02
|
|
|
|
sourceComponent: MaterialIcon {
|
|
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
|
|
|
|
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer
|
|
fontStyle: Tokens.font.icon.large
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Shape {
|
|
id: progressIndicator
|
|
|
|
anchors.centerIn: appIcon
|
|
width: appIcon.implicitWidth + progressShape.strokeWidth * 2
|
|
height: appIcon.implicitHeight + progressShape.strokeWidth * 2
|
|
preferredRendererType: Shape.CurveRenderer
|
|
|
|
ShapePath {
|
|
id: progressShape
|
|
|
|
capStyle: ShapePath.RoundCap
|
|
fillColor: "transparent"
|
|
strokeWidth: 2
|
|
strokeColor: Colours.palette.m3primary
|
|
|
|
PathAngleArc {
|
|
id: progressArc
|
|
|
|
radiusX: progressIndicator.width / 2 - root.Tokens.padding.extraSmall / 2
|
|
centerX: progressIndicator.width / 2
|
|
radiusY: progressIndicator.height / 2 - root.Tokens.padding.extraSmall / 2
|
|
centerY: progressIndicator.height / 2
|
|
|
|
startAngle: -90
|
|
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
|
|
|
|
Behavior on sweepAngle {
|
|
Anim {
|
|
easing: Tokens.anim.emphasizedDecel
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: appName
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: image.right
|
|
anchors.leftMargin: Tokens.spacing.medium
|
|
|
|
animate: true
|
|
text: appNameMetrics.elidedText
|
|
maximumLineCount: 1
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.label.medium
|
|
|
|
opacity: root.expanded ? 1 : 0
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
TextMetrics {
|
|
id: appNameMetrics
|
|
|
|
text: root.modelData.appName
|
|
font: appName.font
|
|
elide: Text.ElideRight
|
|
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3
|
|
}
|
|
|
|
StyledText {
|
|
id: summary
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: image.right
|
|
anchors.leftMargin: Tokens.spacing.medium
|
|
|
|
animate: true
|
|
text: summaryMetrics.elidedText
|
|
maximumLineCount: 1
|
|
height: implicitHeight
|
|
|
|
states: State {
|
|
name: "expanded"
|
|
when: root.expanded
|
|
|
|
PropertyChanges {
|
|
summary.maximumLineCount: undefined
|
|
summary.anchors.topMargin: root.Tokens.spacing.extraSmall
|
|
bodyPreview.anchors.topMargin: root.Tokens.spacing.extraSmall
|
|
body.anchors.topMargin: root.Tokens.spacing.extraSmall
|
|
}
|
|
|
|
AnchorChanges {
|
|
target: summary
|
|
anchors.top: appName.bottom
|
|
}
|
|
}
|
|
|
|
transitions: Transition {
|
|
PropertyAction {
|
|
target: summary
|
|
property: "maximumLineCount"
|
|
}
|
|
Anim {
|
|
property: "topMargin"
|
|
}
|
|
AnchorAnim {}
|
|
}
|
|
|
|
Behavior on height {
|
|
Anim {}
|
|
}
|
|
}
|
|
|
|
TextMetrics {
|
|
id: summaryMetrics
|
|
|
|
text: root.modelData.summary
|
|
font: summary.font
|
|
elide: Text.ElideRight
|
|
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3
|
|
}
|
|
|
|
StyledText {
|
|
id: timeSep
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: summary.right
|
|
anchors.leftMargin: Tokens.spacing.small
|
|
|
|
text: "•"
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.body.small
|
|
|
|
states: State {
|
|
name: "expanded"
|
|
when: root.expanded
|
|
|
|
AnchorChanges {
|
|
target: timeSep
|
|
anchors.left: appName.right
|
|
}
|
|
}
|
|
|
|
transitions: Transition {
|
|
AnchorAnim {}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: time
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: timeSep.right
|
|
anchors.leftMargin: Tokens.spacing.small
|
|
|
|
animate: true
|
|
horizontalAlignment: Text.AlignLeft
|
|
text: root.modelData.timeStr
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.body.small
|
|
}
|
|
|
|
Item {
|
|
id: expandBtn
|
|
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
|
|
implicitWidth: expandIcon.implicitHeight
|
|
implicitHeight: expandIcon.implicitHeight
|
|
|
|
StateLayer {
|
|
radius: Tokens.rounding.full
|
|
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
|
onClicked: root.expanded = !root.expanded
|
|
}
|
|
|
|
MaterialIcon {
|
|
id: expandIcon
|
|
|
|
anchors.centerIn: parent
|
|
text: "expand_more"
|
|
fontStyle: Tokens.font.icon.medium
|
|
rotation: root.expanded ? 180 : 0
|
|
|
|
Behavior on rotation {
|
|
Anim {}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
id: bodyPreview
|
|
|
|
anchors.left: summary.left
|
|
anchors.right: expandBtn.left
|
|
anchors.top: summary.bottom
|
|
anchors.rightMargin: Tokens.spacing.small
|
|
|
|
animate: true
|
|
textFormat: root.bodyTextFormat
|
|
text: bodyPreviewMetrics.elidedText
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.body.small
|
|
|
|
opacity: root.expanded ? 0 : 1
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
TextMetrics {
|
|
id: bodyPreviewMetrics
|
|
|
|
text: root.modelData.body
|
|
font: bodyPreview.font
|
|
elide: Text.ElideRight
|
|
elideWidth: bodyPreview.width
|
|
}
|
|
|
|
StyledText {
|
|
id: body
|
|
|
|
anchors.left: summary.left
|
|
anchors.right: expandBtn.left
|
|
anchors.top: summary.bottom
|
|
anchors.rightMargin: Tokens.spacing.small
|
|
|
|
animate: true
|
|
textFormat: root.bodyTextFormat
|
|
text: root.modelData.body
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.body.small
|
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
height: text ? implicitHeight : 0
|
|
|
|
onLinkActivated: link => {
|
|
if (!root.expanded)
|
|
return;
|
|
|
|
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
|
root.modelData.popup = false;
|
|
}
|
|
|
|
opacity: root.expanded ? 1 : 0
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
ButtonRow {
|
|
id: actions
|
|
|
|
anchors.left: body.left
|
|
anchors.right: body.right
|
|
anchors.top: body.bottom
|
|
anchors.topMargin: Tokens.spacing.small
|
|
|
|
spacing: Tokens.spacing.extraSmall
|
|
opacity: root.expanded ? 1 : 0
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
|
|
IconButton {
|
|
isRound: true
|
|
shapeMorph: true
|
|
fillWidth: root.modelData.actions.length === 0
|
|
inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
|
inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant
|
|
icon: "close"
|
|
padding: Tokens.padding.extraSmall
|
|
onClicked: root.modelData.close()
|
|
}
|
|
|
|
Repeater {
|
|
model: root.modelData.actions
|
|
|
|
TextButton {
|
|
required property var modelData
|
|
|
|
isRound: true
|
|
shapeMorph: true
|
|
fillWidth: true
|
|
inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
|
inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant
|
|
text: modelData.text
|
|
onClicked: modelData.invoke()
|
|
|
|
label.horizontalAlignment: Text.AlignHCenter
|
|
label.anchors.left: left
|
|
label.anchors.right: right
|
|
label.anchors.verticalCenter: verticalCenter
|
|
label.anchors.centerIn: undefined
|
|
label.anchors.margins: Tokens.padding.medium
|
|
label.elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
IconButton {
|
|
isRound: true
|
|
shapeMorph: true
|
|
fillWidth: root.modelData.actions.length === 0
|
|
inactiveColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
|
inactiveOnColour: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant
|
|
icon: copyTimer.running ? "inventory" : "content_copy"
|
|
padding: Tokens.padding.extraSmall
|
|
onClicked: {
|
|
Quickshell.clipboardText = root.modelData.body;
|
|
copyTimer.restart();
|
|
}
|
|
label.animate: true
|
|
|
|
Timer {
|
|
id: copyTimer
|
|
|
|
interval: 3000
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|