feat: better notification storage and image caching + blobs
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
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
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
ButtonRow {
|
||||
id: root
|
||||
|
||||
required property NotificationRow notif
|
||||
required property color buttonColour
|
||||
|
||||
readonly property color fillColour: notif.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : buttonColour
|
||||
readonly property color buttonTextColour: notif.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
||||
|
||||
function invoke(action: var): void {
|
||||
action.invoke();
|
||||
if (!notif.resident)
|
||||
notif.dismiss();
|
||||
}
|
||||
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
fillWidth: root.notif.actions.length === 0
|
||||
inactiveColor: root.fillColour
|
||||
inactiveOnColor: root.buttonTextColour
|
||||
icon: "close"
|
||||
padding: Tokens.padding.extraSmall
|
||||
onClicked: root.notif.dismiss()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.notif.actions
|
||||
|
||||
delegate: root.notif.hasActionIcons ? iconAction : textAction
|
||||
}
|
||||
|
||||
IconButton {
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
fillWidth: root.notif.actions.length === 0
|
||||
inactiveColor: root.fillColour
|
||||
inactiveOnColor: root.buttonTextColour
|
||||
icon: copyTimer.running ? "inventory" : "content_copy"
|
||||
padding: Tokens.padding.extraSmall
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = root.notif.body;
|
||||
copyTimer.restart();
|
||||
}
|
||||
label.animate: true
|
||||
|
||||
Timer {
|
||||
id: copyTimer
|
||||
|
||||
interval: 3000
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textAction
|
||||
|
||||
TextButton {
|
||||
required property var modelData
|
||||
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
fillWidth: true
|
||||
inactiveColor: root.fillColour
|
||||
inactiveOnColor: root.buttonTextColour
|
||||
text: modelData.text
|
||||
onClicked: root.invoke(modelData)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: iconAction
|
||||
|
||||
ButtonBase {
|
||||
id: button
|
||||
|
||||
required property var modelData
|
||||
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
fillWidth: true
|
||||
padding: Tokens.padding.extraSmall
|
||||
inactiveColor: root.fillColour
|
||||
inactiveOnColor: root.buttonTextColour
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + padding * 2
|
||||
onClicked: root.invoke(modelData)
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
|
||||
asynchronous: true
|
||||
source: Quickshell.iconPath(button.modelData.identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import ZShell
|
||||
import M3Shapes
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var owner
|
||||
required property string image
|
||||
required property string icon
|
||||
required property int urgency
|
||||
required property string summary
|
||||
required property color lowFillColour
|
||||
property real progress: 0
|
||||
property string shownImage
|
||||
property string shownIcon
|
||||
|
||||
readonly property color fillColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3primary : urgency === NotificationUrgency.Low ? lowFillColour : Colors.palette.m3secondaryContainer
|
||||
readonly property color iconColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3onPrimary : urgency === NotificationUrgency.Low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSecondaryContainer
|
||||
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
|
||||
// Keeping the previously shown url avoids replaying the fade when a row
|
||||
// swaps between its raw source and the cached blob.
|
||||
onImageChanged: {
|
||||
if (image && !owner.isSameImage(shownImage))
|
||||
shownImage = image;
|
||||
}
|
||||
onIconChanged: {
|
||||
if (icon && !owner.isSameAppIcon(shownIcon))
|
||||
shownIcon = icon;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
shownImage = image;
|
||||
shownIcon = icon;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: imageComp
|
||||
|
||||
FadeImage {
|
||||
source: Qt.resolvedUrl(root.shownImage)
|
||||
width: Config.notifs.sizes.image
|
||||
height: Config.notifs.sizes.image
|
||||
fadeInLargeAnim: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appIconComp
|
||||
|
||||
FadeIcon {
|
||||
size: Math.round(Config.notifs.sizes.image * 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: materialIconComp
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
text: Icons.getNotifIcon(root.summary, root.urgency)
|
||||
color: root.iconColour
|
||||
fontStyle: Tokens.font.icon.medium
|
||||
}
|
||||
}
|
||||
|
||||
MaterialShape {
|
||||
id: bgShape
|
||||
|
||||
anchors.fill: parent
|
||||
shape: contentLoader.sourceComponent === materialIconComp && root.urgency === NotificationUrgency.Critical ? MaterialShape.SoftBurst : MaterialShape.Circle
|
||||
color: Qt.alpha(root.fillColour, 1)
|
||||
opacity: root.fillColour.a
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0
|
||||
asynchronous: true
|
||||
sourceComponent: root.image ? imageComp : root.icon ? appIconComp : materialIconComp
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
asynchronous: true
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
active: opacity > 0
|
||||
opacity: root.icon && root.image ? 1 : 0
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
implicitWidth: Config.notifs.sizes.badge
|
||||
implicitHeight: Config.notifs.sizes.badge
|
||||
|
||||
color: root.fillColour
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
FadeIcon {
|
||||
anchors.centerIn: parent
|
||||
size: Math.round(Config.notifs.sizes.badge * 0.6)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: root.icon && root.image ? Anim.DefaultEffects : Anim.FastEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
asynchronous: true
|
||||
anchors.fill: parent
|
||||
active: root.progress > 0
|
||||
|
||||
sourceComponent: Shape {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: progressShape
|
||||
|
||||
property real sweep: root.progress * 360
|
||||
property real spacing: root.urgency === NotificationUrgency.Critical ? Tokens.spacing.extraSmall : 0
|
||||
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeWidth: 2
|
||||
strokeColor: Colors.palette.m3primary
|
||||
|
||||
PathPolyline {
|
||||
function pointAt(angle: real): point {
|
||||
const dist = bgShape.distanceAtAngle(angle) + progressShape.strokeWidth / 2 + progressShape.spacing;
|
||||
const rad = angle * Math.PI / 180;
|
||||
return Qt.point(root.width / 2 + dist * Math.sin(rad), root.height / 2 - dist * Math.cos(rad));
|
||||
}
|
||||
|
||||
path: {
|
||||
bgShape.morphProgress; // mapToItem-free dependency: re-run when the shape morphs
|
||||
const points = [];
|
||||
for (let angle = 0; angle < progressShape.sweep; angle += 3)
|
||||
points.push(pointAt(angle));
|
||||
points.push(pointAt(progressShape.sweep));
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on sweep {
|
||||
Anim {
|
||||
easing: Tokens.anim.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on spacing {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component FadeIcon: FadeImage {
|
||||
id: fadeIcon
|
||||
|
||||
required property int size
|
||||
|
||||
width: size
|
||||
height: size
|
||||
source: Quickshell.iconPath(root.shownIcon)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
fadeInLargeAnim: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property NotificationGroup group
|
||||
required property Flickable container
|
||||
required property bool expanded
|
||||
property bool expandable: true
|
||||
property int baseSurface: 0
|
||||
property int baseLayer: baseSurface
|
||||
|
||||
readonly property list<NotificationRow> notifs: [...group.activeNotifs]
|
||||
readonly property bool single: notifs.length === 1
|
||||
readonly property alias rowPressed: notifList.rowPressed
|
||||
readonly property list<color> surfaces: [Colors.palette.m3surfaceContainer, Colors.palette.m3surfaceContainerHigh, Colors.palette.m3surfaceContainerHighest]
|
||||
readonly property int nonAnimHeight: {
|
||||
const headerHeight = header.implicitHeight + (expanded ? Tokens.spacing.extraSmall : 0);
|
||||
const columnHeight = headerHeight + notifList.layoutHeight;
|
||||
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.medium * 2);
|
||||
}
|
||||
|
||||
signal requestExpand(expand: bool)
|
||||
signal swiped(notif: NotificationRow)
|
||||
signal linkActivated(notif: NotificationRow)
|
||||
|
||||
function surface(offset: int, layer: int): color {
|
||||
return Colors.layer(surfaces[Math.min(baseSurface + offset, surfaces.length - 1)], baseLayer + layer);
|
||||
}
|
||||
|
||||
Component.onCompleted: group.lock(this)
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: nonAnimHeight
|
||||
|
||||
clip: true
|
||||
radius: Tokens.rounding.large
|
||||
color: group.urgency === NotificationUrgency.Critical && (single || !expanded) ? Colors.palette.m3secondaryContainer : surface(0, 1)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
SnapLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Tokens.padding.medium
|
||||
|
||||
objectName: "notif-content"
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Tokens.spacing.medium
|
||||
|
||||
NotifAvatar {
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
Layout.topMargin: root.expanded && !root.single ? header.implicitHeight + Tokens.spacing.extraSmall + Tokens.padding.extraSmall : 0
|
||||
owner: root.expanded ? root.notifs[0] ?? root.group : root.group
|
||||
image: root.expanded ? root.notifs[0]?.image ?? "" : root.group.image
|
||||
icon: root.expanded ? root.notifs[0]?.appIcon ?? "" : root.group.appIcon
|
||||
urgency: root.expanded ? root.notifs[0]?.urgency ?? NotificationUrgency.Normal : root.group.urgency
|
||||
lowFillColour: root.surface(2, 2)
|
||||
summary: root.notifs[0]?.summary ?? ""
|
||||
progress: (root.notifs[0]?.progress ?? -1) / 100
|
||||
|
||||
Behavior on Layout.topMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: root.expanded ? Tokens.spacing.extraSmall : 0
|
||||
|
||||
Behavior on spacing {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
NotifGroupHeader {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
appName: root.group.appName
|
||||
single: root.single
|
||||
timeStr: root.notifs[0]?.timeStr ?? ""
|
||||
count: root.notifs.length
|
||||
urgency: root.group.urgency
|
||||
pillColour: root.surface(1, 2)
|
||||
expandable: root.expandable
|
||||
expanded: root.expanded
|
||||
onToggle: root.requestExpand(!root.expanded)
|
||||
}
|
||||
|
||||
NotifGroupList {
|
||||
id: notifList
|
||||
|
||||
notifs: root.notifs
|
||||
expanded: root.expanded
|
||||
single: root.single
|
||||
headerHeight: header.implicitHeight
|
||||
headerTitleWidth: header.titleWidth
|
||||
expandable: root.expandable
|
||||
container: root.container
|
||||
itemColour: root.surface(1, 2)
|
||||
avatarColour: root.surface(2, 2)
|
||||
buttonColour: root.surface(2, 3)
|
||||
onRequestExpand: expand => root.requestExpand(expand)
|
||||
onSwiped: notif => root.swiped(notif)
|
||||
onLinkActivated: notif => root.linkActivated(notif)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Services.Notifications
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string appName
|
||||
required property bool single
|
||||
required property string timeStr
|
||||
required property int count
|
||||
required property int urgency
|
||||
required property color pillColour
|
||||
required property bool expandable
|
||||
required property bool expanded
|
||||
readonly property alias titleWidth: appName.width
|
||||
|
||||
readonly property color expandBtnTextColour: urgency === NotificationUrgency.Critical && !single ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
|
||||
|
||||
signal toggle
|
||||
|
||||
implicitHeight: Math.max(appName.implicitHeight, time.implicitHeight, pill.implicitHeight)
|
||||
objectName: "notif-header"
|
||||
|
||||
CustomText {
|
||||
id: appName
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: time.left
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
opacity: root.single && !root.expanded ? 0 : 1
|
||||
text: root.appName
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font: Tokens.font.body.small
|
||||
elide: Text.ElideRight
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: time
|
||||
|
||||
anchors.right: pill.left
|
||||
anchors.rightMargin: Tokens.spacing.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
animate: true
|
||||
text: root.timeStr
|
||||
color: {
|
||||
const col = root.urgency === NotificationUrgency.Critical && (root.single || !root.expanded) ? Colors.palette.m3secondary : Colors.palette.m3outline;
|
||||
return root.expanded && !root.single ? Qt.alpha(col, 0) : col;
|
||||
}
|
||||
font: Tokens.font.body.small
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: pill
|
||||
|
||||
property real horizontalPadding: root.single ? implicitHeight - expandIcon.implicitWidth : Tokens.padding.large
|
||||
property real countSpan: root.single ? 0 : groupCount.implicitWidth + Tokens.spacing.extraSmall
|
||||
property real iconInset: root.single ? 0 : Tokens.padding.extraSmall / 2
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
enabled: root.expandable
|
||||
// StateLayer inherits this through its parent.enabled binding, so a
|
||||
// non-expandable group gets no hover, ripple, or cursor.
|
||||
|
||||
implicitWidth: horizontalPadding + countSpan + (root.expandable ? expandIcon.implicitWidth : 0)
|
||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
||||
|
||||
color: {
|
||||
const c = root.urgency === NotificationUrgency.Critical ? Colors.palette.m3primary : root.pillColour;
|
||||
return root.single ? Qt.alpha(c, 0) : c;
|
||||
}
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
color: root.expandBtnTextColour
|
||||
onClicked: root.toggle()
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: pill.horizontalPadding / 2 + pill.iconInset
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
objectName: "notif-expand"
|
||||
|
||||
implicitWidth: groupCount.implicitWidth
|
||||
implicitHeight: groupCount.implicitHeight
|
||||
opacity: root.single ? 0 : 1
|
||||
|
||||
CustomText {
|
||||
id: groupCount
|
||||
|
||||
animate: true
|
||||
text: root.count
|
||||
color: root.expandBtnTextColour
|
||||
font: Tokens.font.body.small
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: pill.horizontalPadding / 2 - pill.iconInset
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: root.expanded && !root.single ? -Tokens.padding.extraSmall / 2 : 0
|
||||
|
||||
visible: root.expandable
|
||||
text: "expand_more"
|
||||
color: root.expandBtnTextColour
|
||||
rotation: root.expanded ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on horizontalPadding {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on countSpan {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on iconInset {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
LazyListView {
|
||||
id: root
|
||||
|
||||
required property list<NotificationRow> notifs
|
||||
required property bool expanded
|
||||
required property bool expandable
|
||||
required property bool single
|
||||
required property Flickable container
|
||||
property real headerHeight
|
||||
property real headerTitleWidth
|
||||
required property color itemColour
|
||||
required property color avatarColour
|
||||
required property color buttonColour
|
||||
readonly property bool rowPressed: pressedRows > 0
|
||||
property int pressedRows
|
||||
|
||||
signal requestExpand(expand: bool)
|
||||
signal swiped(notif: NotificationRow)
|
||||
signal linkActivated(notif: NotificationRow)
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentHeight
|
||||
|
||||
spacing: Math.round(Tokens.spacing.extraSmall)
|
||||
asynchronous: true
|
||||
|
||||
readyDelay: 1
|
||||
cacheBuffer: 400
|
||||
removeDuration: Tokens.anim.durations.normal
|
||||
|
||||
useCustomViewport: true
|
||||
viewport: {
|
||||
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
|
||||
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
// Spread, not `as Array`: `notifs` is a list<NotificationRow> property,
|
||||
// and reading it yields a list reference that ScriptModel cannot assign.
|
||||
values: root.expanded ? [...root.notifs] : [...root.notifs].slice(0, Config.notifs.groupPreviewNum);
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
NotifSwipeArea {
|
||||
id: notif
|
||||
|
||||
required property int index
|
||||
required property NotificationRow modelData
|
||||
|
||||
Component.onCompleted: modelData.ref(this)
|
||||
Component.onDestruction: {
|
||||
if (pressed)
|
||||
root.pressedRows--;
|
||||
}
|
||||
|
||||
LazyListView.preferredHeight: LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
||||
LazyListView.visibleHeight: LazyListView.removing ? 0 : notifInner.implicitHeight
|
||||
implicitHeight: notifInner.implicitHeight
|
||||
|
||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||
|
||||
expanded: root.expanded
|
||||
expandable: root.expandable
|
||||
dragExpand: false
|
||||
hoveringLink: !!notifInner.body?.hoveredLink
|
||||
enabled: root.expandable && root.expanded && !LazyListView.removing
|
||||
acceptedButtons: root.single ? Qt.NoButton : Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
|
||||
onPressedChanged: root.pressedRows += pressed ? 1 : -1
|
||||
onRequestExpand: expand => root.requestExpand(expand)
|
||||
onSwiped: root.swiped(modelData)
|
||||
onDismissed: modelData.dismiss()
|
||||
|
||||
Connections {
|
||||
function onClosedChanged(): void {
|
||||
if (notif.modelData.closed)
|
||||
closeAnim.start();
|
||||
}
|
||||
|
||||
target: notif.modelData
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: closeAnim
|
||||
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
target: notif
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: notif.x >= 0 ? notif.width : -notif.width
|
||||
}
|
||||
}
|
||||
|
||||
NotifItem {
|
||||
id: notifInner
|
||||
|
||||
anchors.fill: parent
|
||||
modelData: notif.modelData
|
||||
expanded: root.expanded
|
||||
single: root.single
|
||||
ready: notif.LazyListView.ready
|
||||
headerHeight: root.headerHeight
|
||||
headerTitleWidth: root.headerTitleWidth
|
||||
showAvatar: notif.index > 0
|
||||
expandedColour: root.itemColour
|
||||
avatarColour: root.avatarColour
|
||||
buttonColour: root.buttonColour
|
||||
onLinkActivated: root.linkActivated(notif.modelData)
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: notif.LazyListView.ready
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TransformWatcher {
|
||||
id: tWatcher
|
||||
|
||||
a: root.container.contentItem
|
||||
b: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
required property bool expanded
|
||||
property bool expandable: true
|
||||
property bool dragExpand: expandable
|
||||
property bool hoveringLink
|
||||
property int startY
|
||||
property real lastX
|
||||
property real lastMoveTime
|
||||
property real velocity
|
||||
|
||||
signal swiped
|
||||
signal dismissed
|
||||
signal requestExpand(expand: bool)
|
||||
|
||||
hoverEnabled: true
|
||||
cursorShape: hoveringLink ? Qt.PointingHandCursor : pressedButtons === Qt.LeftButton ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
preventStealing: dragExpand
|
||||
|
||||
drag.target: pressedButtons === Qt.LeftButton ? this : null
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
lastX = x;
|
||||
lastMoveTime = Date.now();
|
||||
velocity = 0;
|
||||
if (event.button === Qt.RightButton && expandable)
|
||||
requestExpand(!expanded);
|
||||
else if (event.button === Qt.MiddleButton)
|
||||
dismissed();
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressedButtons !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
const now = Date.now();
|
||||
const dt = now - lastMoveTime;
|
||||
if (dt > 0) {
|
||||
velocity = (velocity + (x - lastX) / dt * 1000) / 2;
|
||||
lastX = x;
|
||||
lastMoveTime = now;
|
||||
}
|
||||
|
||||
if (dragExpand) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||
requestExpand(diffY > 0);
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
const flung = Date.now() - lastMoveTime < 100 && Math.sign(velocity) === Math.sign(x) && Math.abs(velocity) > Config.notifs.clearVelocity;
|
||||
if (!flung && Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||
x = 0;
|
||||
else
|
||||
swiped();
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user