Files
z-bar-qt/Components/Notifs/NotifItem.qml
T
zach 11e7aac195
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
feat: better notification storage and image caching + blobs
2026-11-02 19:00:43 +01:00

288 lines
6.8 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Notifications
import ZShell.Components
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
CustomRect {
id: root
required property NotificationRow modelData
required property bool expanded
required property color expandedColour
required property color avatarColour
required property color buttonColour
property bool showAvatar: true
property bool single: false
property bool ready: true
property real headerHeight
property real headerTitleWidth
readonly property bool critical: modelData.urgency === NotificationUrgency.Critical
readonly property bool low: modelData.urgency === NotificationUrgency.Low
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
readonly property real padding: single ? 0 : Tokens.padding.medium
readonly property real headerSummaryOffset: -(headerHeight + summaryHeightMetrics.height) / 2
readonly property real inset: expanded ? padding : 0
readonly property real summaryOffset: single && !expanded ? headerSummaryOffset : inset
readonly property real nonAnimHeight: {
if (!expanded)
return summaryHeightMetrics.height;
const contentHeight = summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + padding * 2;
return single ? contentHeight : Math.max(contentHeight, Config.notifs.sizes.image + Tokens.padding.extraSmall * 2);
}
signal linkActivated
implicitHeight: nonAnimHeight
radius: Tokens.rounding.medium
color: {
const c = root.critical ? Colors.palette.m3secondaryContainer : root.expandedColour;
return expanded && !single ? c : Qt.alpha(c, 0);
}
TextMetrics {
id: summaryHeightMetrics
font: dummySummary.font
text: " " // Use this height to prevent weird characters from changing the line height
}
TextMetrics {
id: summaryMetrics
font: dummySummary.font
text: root.modelData.summary
elide: Text.ElideRight
elideWidth: root.single ? root.headerTitleWidth : root.width
}
WrappedLoader {
id: avatar
shouldBeActive: root.expanded && root.showAvatar
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: Tokens.padding.extraSmall
anchors.leftMargin: -(Config.notifs.sizes.image + Tokens.spacing.medium)
sourceComponent: NotifAvatar {
owner: root.modelData
image: root.modelData.image
icon: root.modelData.appIcon
urgency: root.modelData.urgency
lowFillColour: root.avatarColour
summary: root.modelData.summary
progress: root.modelData.progress / 100
}
}
WrappedLoader {
id: compactSummary
shouldBeActive: !root.expanded
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: root.inset
anchors.topMargin: root.summaryOffset
sourceComponent: CustomText {
animate: root.ready
text: summaryMetrics.elidedText
color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface
}
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
}
CustomText {
id: dummySummary
anchors.top: parent.top
anchors.left: parent.left
visible: false
text: root.modelData.summary
}
WrappedLoader {
id: compactBody
shouldBeActive: !root.expanded
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: root.inset + (root.single ? 0 : dummySummary.width + Tokens.spacing.small)
anchors.topMargin: root.single && root.expanded ? summary.implicitHeight + expandedContent.anchors.topMargin : root.inset
sourceComponent: CustomText {
text: root.modelData.body.replace(/\n/g, " ")
color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
}
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
}
WrappedLoader {
id: summary
shouldBeActive: root.expanded
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: root.inset
anchors.topMargin: root.summaryOffset
width: root.width - root.padding * 2 - timeStr.implicitWidth - Tokens.spacing.small
sourceComponent: CustomText {
text: root.modelData.summary
color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface
wrapMode: Text.WordWrap
}
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
}
WrappedLoader {
id: timeStr
shouldBeActive: root.expanded && !root.single
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: root.inset
sourceComponent: CustomText {
animate: true
text: root.modelData.timeStr
color: root.critical ? Colors.palette.m3secondary : Colors.palette.m3outline
font: Tokens.font.body.small
}
Behavior on anchors.margins {
Anim {}
}
}
WrappedLoader {
id: expandedContent
shouldBeActive: root.expanded
anchors.top: summary.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: root.inset
anchors.topMargin: Tokens.spacing.extraSmall
sourceComponent: ExpandedBody {}
Behavior on anchors.margins {
Anim {}
}
}
Behavior on implicitHeight {
Anim {}
}
component ExpandedBody: SnapLayout {
readonly property alias body: bodyText
orientation: Qt.Vertical
spacing: Tokens.spacing.medium
CustomText {
id: bodyText
Layout.fillWidth: true
textFormat: Text.MarkdownText
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/")
color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant
wrapMode: Text.WordWrap
onLinkActivated: link => {
Qt.openUrlExternally(link);
root.linkActivated();
}
}
NotifActions {
Layout.fillWidth: true
notif: root.modelData
buttonColour: root.buttonColour
}
}
component WrappedLoader: Loader {
id: comp
required property bool shouldBeActive
active: false
opacity: 0
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
states: State {
name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.opacity: 1
comp.active: true
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
type: Anim.DefaultEffects
property: "opacity"
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
type: Anim.DefaultEffects
property: "opacity"
}
PropertyAction {
property: "active"
}
}
}
]
}
}