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:
@@ -51,7 +51,7 @@ CustomRect {
|
||||
if (internalChecked)
|
||||
return checkedRadius;
|
||||
if (isRound)
|
||||
return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
|
||||
return (height ?? implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale);
|
||||
return defaultRadius;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import ZShell
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
@@ -9,21 +10,50 @@ import qs.Services
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
|
||||
// Ring geometry, quantized in the item's own device pixel space and never
|
||||
// against its position: the parent layout places the item on the device
|
||||
// grid, so these values cannot change when the item moves (no jitter), and
|
||||
// the ring stays crisp wherever it lands.
|
||||
// Device-pixel center of the ring: an even stroke width wants a pixel
|
||||
// boundary, an odd one a pixel center. Floored, matching
|
||||
// WavyLineRenderer::snapCenterDev, so the wave and this background arc
|
||||
// always land on the same center even when the box's device size is odd.
|
||||
readonly property real centerDev: Math.floor(
|
||||
sizeDev / 2 - strokeParity / 2
|
||||
) + strokeParity / 2
|
||||
readonly property real centerX: centerDev / dpr
|
||||
readonly property real centerY: centerDev / dpr
|
||||
// Whole device pixels, and one pixel of slack on the outer edge: the ring's
|
||||
// antialias feather needs somewhere to fade, otherwise it is cut flat at
|
||||
// the top/left/right/bottom of the item rect.
|
||||
readonly property real arcRadius: Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
(sizeDev - Math.round(padding * dpr) - strokeDev * (1 + waveAmplitude * 2) - 2) / 2
|
||||
)
|
||||
) / dpr
|
||||
property color bgColor: Colors.palette.m3secondaryContainer
|
||||
property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value))
|
||||
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
|
||||
readonly property real dpr: ZUtils.dprFor(root)
|
||||
property color fgColor: Colors.palette.m3primary
|
||||
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
||||
readonly property real gapAngle: ((spacing + strokeLogical) / (arcRadius || 1)) * (180 / Math.PI)
|
||||
property alias hasEndIndicator: dot.active
|
||||
property real implicitSize
|
||||
property int padding: 0
|
||||
readonly property real size: Math.min(width, height)
|
||||
// The item's own size in whole device pixels. Never adjusted upwards: the
|
||||
// texture is exactly this wide, so ring values derived from it cannot spill
|
||||
// past the item's bounds.
|
||||
readonly property real size: sizeDev / dpr
|
||||
readonly property real sizeDev: Math.round(Math.min(width, height) * dpr)
|
||||
property int spacing: Tokens.spacing.small
|
||||
readonly property real strokeDev: Math.max(1, Math.round(strokeWidth * dpr))
|
||||
readonly property real strokeLogical: strokeDev / dpr
|
||||
readonly property real strokeParity: strokeDev % 2
|
||||
property int startAngle: -90
|
||||
property int strokeWidth: Tokens.padding.extraSmall
|
||||
property int sweepAngle: 360
|
||||
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
|
||||
readonly property real thickness: strokeLogical * (1 + waveAmplitude) * 2
|
||||
property real value
|
||||
property alias waveAmplitude: wave.amplitudeMultiplier
|
||||
property alias waveDuration: waveProgAnim.duration
|
||||
@@ -43,7 +73,7 @@ Item {
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.bgColor
|
||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
||||
strokeWidth: Math.max(1, Math.round(Math.min(1, remainingArc.sweepAngle) * root.strokeDev)) / root.dpr
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {
|
||||
@@ -53,8 +83,8 @@ Item {
|
||||
PathAngleArc {
|
||||
id: remainingArc
|
||||
|
||||
centerX: root.size / 2
|
||||
centerY: root.size / 2
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
|
||||
@@ -68,11 +98,12 @@ Item {
|
||||
|
||||
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
||||
anchors.fill: parent
|
||||
anchors.margins: -lineWidth * amplitudeMultiplier
|
||||
anchors.margins: -Math.round(lineWidth * amplitudeMultiplier * root.dpr) / root.dpr
|
||||
color: root.fgColor
|
||||
frequency: 8
|
||||
fullAngle: root.sweepAngle
|
||||
lineWidth: root.strokeWidth
|
||||
objectName: "wave"
|
||||
pathType: WavyLine.Arc
|
||||
radius: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
@@ -103,8 +134,11 @@ Item {
|
||||
Loader {
|
||||
id: dot
|
||||
|
||||
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
|
||||
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
|
||||
// Rounded in local device pixels: the dot's own size is a whole even
|
||||
// device count, so an integral local position puts its center on the grid
|
||||
// whatever the ring's angle, and moving the gauge moves the dot with it.
|
||||
x: Math.round((root.centerX + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2) * root.dpr) / root.dpr
|
||||
y: Math.round((root.centerY + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2) * root.dpr) / root.dpr
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: root.fgColor
|
||||
|
||||
@@ -96,6 +96,8 @@ ProgressBar {
|
||||
|
||||
anchors.right: parent.right
|
||||
implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
|
||||
Line {
|
||||
@@ -197,7 +199,6 @@ ProgressBar {
|
||||
component Line: CustomRect {
|
||||
property point bounds
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.bgColor
|
||||
implicitHeight: parent.height
|
||||
implicitWidth: bounds.y - bounds.x
|
||||
|
||||
@@ -421,7 +421,6 @@ Item {
|
||||
|
||||
var now = Date.now();
|
||||
var isTouchpad = !isMouseWheel(wheel) && (wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0));
|
||||
console.log("wheel", wheel.angleDelta.y, wheel.pixelDelta.y, wheel.phase);
|
||||
if (!isTouchpad) {
|
||||
velocity = 0;
|
||||
lastWheelTimestamp = 0;
|
||||
|
||||
@@ -55,6 +55,21 @@ Item {
|
||||
return ZUtils.snap(v, canvas);
|
||||
}
|
||||
|
||||
// A value is plottable only if it is a real number. Everything else -
|
||||
// null, undefined, NaN, an unparsable string from the API - is a hole, so
|
||||
// the series splits there. Non-finite coordinates must never reach the
|
||||
// path: one NaN lineTo makes the canvas ignore the rest of the curve, and
|
||||
// the fill then runs from whatever y it last had straight down.
|
||||
function numeric(v) {
|
||||
if (typeof v === "number")
|
||||
return Number.isFinite(v) ? v : null;
|
||||
if (typeof v === "string") {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function strokeCurve(ctx, pts) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pts[0].x, pts[0].y);
|
||||
@@ -230,7 +245,8 @@ Item {
|
||||
if (loAuto || hiAuto) {
|
||||
let dmin = Infinity;
|
||||
let dmax = -Infinity;
|
||||
for (const v of root.values) {
|
||||
for (const value of root.values) {
|
||||
const v = numeric(value);
|
||||
if (v == null)
|
||||
continue;
|
||||
if (v < dmin)
|
||||
@@ -272,7 +288,7 @@ Item {
|
||||
if (range <= 0 || root.xSteps < 1)
|
||||
return;
|
||||
|
||||
let series = root.values;
|
||||
let series = root.values.map(v => numeric(v));
|
||||
if (root.smoothing > 0 && series.length > 0) {
|
||||
const radius = Math.round(root.smoothing);
|
||||
const raw = series;
|
||||
|
||||
@@ -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